mcp-dbutils 0.23.1__py3-none-any.whl → 1.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
mcp_dbutils/base.py CHANGED
@@ -12,6 +12,7 @@ import mcp.types as types
12
12
  import yaml
13
13
  from mcp.server import Server
14
14
 
15
+ from .audit import format_logs, get_logs, log_write_operation
15
16
  from .log import create_logger
16
17
  from .stats import ResourceStats
17
18
 
@@ -42,6 +43,10 @@ EMPTY_TABLE_NAME_ERROR = "Table name cannot be empty"
42
43
  CONNECTION_NAME_REQUIRED_ERROR = "Connection name must be specified"
43
44
  SELECT_ONLY_ERROR = "Only SELECT queries are supported for security reasons"
44
45
  INVALID_URI_FORMAT_ERROR = "Invalid resource URI format"
46
+ CONNECTION_NOT_WRITABLE_ERROR = "This connection is not configured for write operations. Add 'writable: true' to the connection configuration."
47
+ WRITE_OPERATION_NOT_ALLOWED_ERROR = "No permission to perform {operation} operation on table {table}."
48
+ WRITE_CONFIRMATION_REQUIRED_ERROR = "Operation not confirmed. To execute write operations, you must set confirmation='CONFIRM_WRITE'."
49
+ UNSUPPORTED_WRITE_OPERATION_ERROR = "Unsupported SQL operation: {operation}. Only INSERT, UPDATE, DELETE are supported."
45
50
 
46
51
  # 获取包信息用于日志命名
47
52
  pkg_meta = metadata("mcp-dbutils")
@@ -116,6 +121,11 @@ class ConnectionHandler(ABC):
116
121
  """Internal query execution method to be implemented by subclasses"""
117
122
  pass
118
123
 
124
+ @abstractmethod
125
+ async def _execute_write_query(self, sql: str) -> str:
126
+ """Internal write query execution method to be implemented by subclasses"""
127
+ pass
128
+
119
129
  async def execute_query(self, sql: str) -> str:
120
130
  """Execute SQL query with performance tracking"""
121
131
  start_time = datetime.now()
@@ -139,6 +149,170 @@ class ConnectionHandler(ABC):
139
149
  )
140
150
  raise
141
151
 
152
+ async def execute_write_query(self, sql: str) -> str:
153
+ """Execute SQL write query with performance tracking
154
+
155
+ Args:
156
+ sql: SQL write query (INSERT, UPDATE, DELETE)
157
+
158
+ Returns:
159
+ str: Execution result
160
+
161
+ Raises:
162
+ ValueError: If the SQL is not a write operation
163
+ """
164
+ # Validate SQL type
165
+ sql_type = self._get_sql_type(sql)
166
+ if sql_type not in ["INSERT", "UPDATE", "DELETE"]:
167
+ raise ValueError(UNSUPPORTED_WRITE_OPERATION_ERROR.format(operation=sql_type))
168
+
169
+ # Extract table name
170
+ table_name = self._extract_table_name(sql)
171
+
172
+ start_time = datetime.now()
173
+ affected_rows = 0
174
+ status = "SUCCESS"
175
+ error_message = None
176
+
177
+ try:
178
+ self.stats.record_query()
179
+ self.send_log(
180
+ LOG_LEVEL_INFO,
181
+ f"Executing write operation: {sql_type} on table {table_name}",
182
+ )
183
+
184
+ result = await self._execute_write_query(sql)
185
+
186
+ # 尝试从结果中提取受影响的行数
187
+ try:
188
+ if "row" in result and "affected" in result:
189
+ # 从结果字符串中提取受影响的行数
190
+ import re
191
+ # 限制数字长度,避免DoS风险
192
+ match = re.search(r"(\d{1,10}) rows?", result)
193
+ if match:
194
+ affected_rows = int(match.group(1))
195
+ except Exception:
196
+ # 如果无法提取,使用默认值
197
+ affected_rows = 1
198
+
199
+ duration = (datetime.now() - start_time).total_seconds()
200
+ self.stats.record_query_duration(sql, duration)
201
+ self.stats.update_memory_usage(result)
202
+
203
+ # 记录审计日志
204
+ log_write_operation(
205
+ connection_name=self.connection,
206
+ table_name=table_name,
207
+ operation_type=sql_type,
208
+ sql=sql,
209
+ affected_rows=affected_rows,
210
+ execution_time=duration * 1000, # 转换为毫秒
211
+ status=status,
212
+ error_message=error_message
213
+ )
214
+
215
+ self.send_log(
216
+ LOG_LEVEL_INFO,
217
+ f"Write operation executed in {duration * 1000:.2f}ms. Resource stats: {json.dumps(self.stats.to_dict())}",
218
+ )
219
+ return result
220
+ except Exception as e:
221
+ duration = (datetime.now() - start_time).total_seconds()
222
+ self.stats.record_error(e.__class__.__name__)
223
+ status = "FAILED"
224
+ error_message = str(e)
225
+
226
+ # 记录审计日志(失败)
227
+ log_write_operation(
228
+ connection_name=self.connection,
229
+ table_name=table_name,
230
+ operation_type=sql_type,
231
+ sql=sql,
232
+ affected_rows=0,
233
+ execution_time=duration * 1000, # 转换为毫秒
234
+ status=status,
235
+ error_message=error_message
236
+ )
237
+
238
+ self.send_log(
239
+ LOG_LEVEL_ERROR,
240
+ f"Write operation error after {duration * 1000:.2f}ms - {str(e)}\nResource stats: {json.dumps(self.stats.to_dict())}",
241
+ )
242
+ raise
243
+
244
+ def _get_sql_type(self, sql: str) -> str:
245
+ """Get SQL statement type
246
+
247
+ Args:
248
+ sql: SQL statement
249
+
250
+ Returns:
251
+ str: SQL statement type (SELECT, INSERT, UPDATE, DELETE, etc.)
252
+ """
253
+ sql = sql.strip().upper()
254
+ if sql.startswith("SELECT"):
255
+ return "SELECT"
256
+ elif sql.startswith("INSERT"):
257
+ return "INSERT"
258
+ elif sql.startswith("UPDATE"):
259
+ return "UPDATE"
260
+ elif sql.startswith("DELETE"):
261
+ return "DELETE"
262
+ elif sql.startswith("CREATE"):
263
+ return "CREATE"
264
+ elif sql.startswith("ALTER"):
265
+ return "ALTER"
266
+ elif sql.startswith("DROP"):
267
+ return "DROP"
268
+ elif sql.startswith("TRUNCATE"):
269
+ return "TRUNCATE"
270
+ elif sql.startswith("BEGIN") or sql.startswith("START"):
271
+ return "TRANSACTION_START"
272
+ elif sql.startswith("COMMIT"):
273
+ return "TRANSACTION_COMMIT"
274
+ elif sql.startswith("ROLLBACK"):
275
+ return "TRANSACTION_ROLLBACK"
276
+ else:
277
+ return "UNKNOWN"
278
+
279
+ def _extract_table_name(self, sql: str) -> str:
280
+ """Extract table name from SQL statement
281
+
282
+ This is a simple implementation that works for basic SQL statements.
283
+ Subclasses may override this method to provide more accurate table name extraction.
284
+
285
+ Args:
286
+ sql: SQL statement
287
+
288
+ Returns:
289
+ str: Table name
290
+ """
291
+ sql_type = self._get_sql_type(sql)
292
+ sql = sql.strip()
293
+
294
+ if sql_type == "INSERT":
295
+ # INSERT INTO table_name ...
296
+ match = sql.upper().split("INTO", 1)
297
+ if len(match) > 1:
298
+ table_part = match[1].strip().split(" ", 1)[0]
299
+ return table_part.strip('`"[]')
300
+ elif sql_type == "UPDATE":
301
+ # UPDATE table_name ...
302
+ match = sql.upper().split("UPDATE", 1)
303
+ if len(match) > 1:
304
+ table_part = match[1].strip().split(" ", 1)[0]
305
+ return table_part.strip('`"[]')
306
+ elif sql_type == "DELETE":
307
+ # DELETE FROM table_name ...
308
+ match = sql.upper().split("FROM", 1)
309
+ if len(match) > 1:
310
+ table_part = match[1].strip().split(" ", 1)[0]
311
+ return table_part.strip('`"[]')
312
+
313
+ # Default fallback
314
+ return "unknown_table"
315
+
142
316
  @abstractmethod
143
317
  async def get_table_description(self, table_name: str) -> str:
144
318
  """Get detailed table description including columns, types, and comments
@@ -356,6 +530,135 @@ class ConnectionServer:
356
530
 
357
531
  return db_config
358
532
 
533
+ def _get_sql_type(self, sql: str) -> str:
534
+ """Get SQL statement type
535
+
536
+ Args:
537
+ sql: SQL statement
538
+
539
+ Returns:
540
+ str: SQL statement type (SELECT, INSERT, UPDATE, DELETE, etc.)
541
+ """
542
+ sql = sql.strip().upper()
543
+ if sql.startswith("SELECT"):
544
+ return "SELECT"
545
+ elif sql.startswith("INSERT"):
546
+ return "INSERT"
547
+ elif sql.startswith("UPDATE"):
548
+ return "UPDATE"
549
+ elif sql.startswith("DELETE"):
550
+ return "DELETE"
551
+ elif sql.startswith("CREATE"):
552
+ return "CREATE"
553
+ elif sql.startswith("ALTER"):
554
+ return "ALTER"
555
+ elif sql.startswith("DROP"):
556
+ return "DROP"
557
+ elif sql.startswith("TRUNCATE"):
558
+ return "TRUNCATE"
559
+ elif sql.startswith("BEGIN") or sql.startswith("START"):
560
+ return "TRANSACTION_START"
561
+ elif sql.startswith("COMMIT"):
562
+ return "TRANSACTION_COMMIT"
563
+ elif sql.startswith("ROLLBACK"):
564
+ return "TRANSACTION_ROLLBACK"
565
+ else:
566
+ return "UNKNOWN"
567
+
568
+ def _extract_table_name(self, sql: str) -> str:
569
+ """Extract table name from SQL statement
570
+
571
+ This is a simple implementation that works for basic SQL statements.
572
+
573
+ Args:
574
+ sql: SQL statement
575
+
576
+ Returns:
577
+ str: Table name
578
+ """
579
+ sql_type = self._get_sql_type(sql)
580
+ sql = sql.strip()
581
+
582
+ if sql_type == "INSERT":
583
+ # INSERT INTO table_name ...
584
+ match = sql.upper().split("INTO", 1)
585
+ if len(match) > 1:
586
+ table_part = match[1].strip().split(" ", 1)[0]
587
+ return table_part.strip('`"[]')
588
+ elif sql_type == "UPDATE":
589
+ # UPDATE table_name ...
590
+ match = sql.upper().split("UPDATE", 1)
591
+ if len(match) > 1:
592
+ table_part = match[1].strip().split(" ", 1)[0]
593
+ return table_part.strip('`"[]')
594
+ elif sql_type == "DELETE":
595
+ # DELETE FROM table_name ...
596
+ match = sql.upper().split("FROM", 1)
597
+ if len(match) > 1:
598
+ table_part = match[1].strip().split(" ", 1)[0]
599
+ return table_part.strip('`"[]')
600
+
601
+ # Default fallback
602
+ return "unknown_table"
603
+
604
+ async def _check_write_permission(self, connection: str, table_name: str, operation_type: str) -> None:
605
+ """检查写操作权限
606
+
607
+ Args:
608
+ connection: 数据库连接名称
609
+ table_name: 表名
610
+ operation_type: 操作类型 (INSERT, UPDATE, DELETE)
611
+
612
+ Raises:
613
+ ConfigurationError: 如果连接不可写或没有表级权限
614
+ """
615
+ # 获取连接配置
616
+ db_config = self._get_config_or_raise(connection)
617
+
618
+ # 检查连接是否可写
619
+ if not db_config.get("writable", False):
620
+ raise ConfigurationError(CONNECTION_NOT_WRITABLE_ERROR)
621
+
622
+ # 检查是否有写权限配置
623
+ write_permissions = db_config.get("write_permissions", {})
624
+ if not write_permissions:
625
+ # 没有细粒度权限控制,默认允许所有写操作
626
+ return
627
+
628
+ # 检查表级权限
629
+ tables = write_permissions.get("tables", {})
630
+ if not tables:
631
+ # 没有表级权限配置,检查默认策略
632
+ default_policy = write_permissions.get("default_policy", "read_only")
633
+ if default_policy == "allow_all":
634
+ return
635
+ else:
636
+ # 默认只读
637
+ raise ConfigurationError(WRITE_OPERATION_NOT_ALLOWED_ERROR.format(
638
+ operation=operation_type, table=table_name
639
+ ))
640
+
641
+ # 检查特定表的权限
642
+ if table_name in tables:
643
+ table_config = tables[table_name]
644
+ operations = table_config.get("operations", ["INSERT", "UPDATE", "DELETE"])
645
+ if operation_type in operations:
646
+ return
647
+ else:
648
+ raise ConfigurationError(WRITE_OPERATION_NOT_ALLOWED_ERROR.format(
649
+ operation=operation_type, table=table_name
650
+ ))
651
+ else:
652
+ # 表未明确配置,检查默认策略
653
+ default_policy = write_permissions.get("default_policy", "read_only")
654
+ if default_policy == "allow_all":
655
+ return
656
+ else:
657
+ # 默认只读
658
+ raise ConfigurationError(WRITE_OPERATION_NOT_ALLOWED_ERROR.format(
659
+ operation=operation_type, table=table_name
660
+ ))
661
+
359
662
  def _create_handler_for_type(
360
663
  self, db_type: str, connection: str
361
664
  ) -> ConnectionHandler:
@@ -457,6 +760,54 @@ class ConnectionServer:
457
760
  "required": [],
458
761
  },
459
762
  ),
763
+ types.Tool(
764
+ name="dbutils-execute-write",
765
+ description="CAUTION: This tool executes data modification operations (INSERT, UPDATE, DELETE) on the specified database. It requires explicit configuration and confirmation. Only available for connections with 'writable: true' in configuration. All operations are logged for audit purposes.",
766
+ inputSchema={
767
+ "type": "object",
768
+ "properties": {
769
+ "connection": {
770
+ "type": "string",
771
+ "description": DATABASE_CONNECTION_NAME,
772
+ },
773
+ "sql": {
774
+ "type": "string",
775
+ "description": "SQL statement (INSERT, UPDATE, DELETE)",
776
+ },
777
+ "confirmation": {
778
+ "type": "string",
779
+ "description": "Type 'CONFIRM_WRITE' to confirm you understand the risks",
780
+ },
781
+ },
782
+ "required": ["connection", "sql", "confirmation"],
783
+ },
784
+ annotations={
785
+ "examples": [
786
+ {
787
+ "input": {
788
+ "connection": "example_db",
789
+ "sql": "INSERT INTO logs (event, timestamp) VALUES ('event1', CURRENT_TIMESTAMP)",
790
+ "confirmation": "CONFIRM_WRITE"
791
+ },
792
+ "output": "Write operation executed successfully. 1 row affected."
793
+ },
794
+ {
795
+ "input": {
796
+ "connection": "example_db",
797
+ "sql": "UPDATE users SET status = 'active' WHERE id = 123",
798
+ "confirmation": "CONFIRM_WRITE"
799
+ },
800
+ "output": "Write operation executed successfully. 1 row affected."
801
+ }
802
+ ],
803
+ "usage_tips": [
804
+ "Always confirm with 'CONFIRM_WRITE' to execute write operations",
805
+ "Connection must have 'writable: true' in configuration",
806
+ "Consider using transactions for multiple related operations",
807
+ "Check audit logs after write operations to verify changes"
808
+ ]
809
+ }
810
+ ),
460
811
  types.Tool(
461
812
  name="dbutils-run-query",
462
813
  description="Executes read-only SQL queries on the specified database connection. For security, only SELECT statements are supported. Returns structured results with column names and data rows. Supports complex queries including JOINs, GROUP BY, ORDER BY, and aggregate functions. Use this tool when you need to analyze data, validate hypotheses, or extract specific information. Query execution is protected by resource limits and timeouts to prevent system resource overuse.",
@@ -665,6 +1016,39 @@ class ConnectionServer:
665
1016
  "required": ["connection", "sql"],
666
1017
  },
667
1018
  ),
1019
+ types.Tool(
1020
+ name="dbutils-get-audit-logs",
1021
+ description="Retrieves audit logs for database write operations. Shows who performed what operations, when, and with what results. Useful for security monitoring, compliance, and troubleshooting.",
1022
+ inputSchema={
1023
+ "type": "object",
1024
+ "properties": {
1025
+ "connection": {
1026
+ "type": "string",
1027
+ "description": "Filter logs by connection name",
1028
+ },
1029
+ "table": {
1030
+ "type": "string",
1031
+ "description": "Filter logs by table name",
1032
+ },
1033
+ "operation_type": {
1034
+ "type": "string",
1035
+ "description": "Filter logs by operation type (INSERT, UPDATE, DELETE)",
1036
+ "enum": ["INSERT", "UPDATE", "DELETE"]
1037
+ },
1038
+ "status": {
1039
+ "type": "string",
1040
+ "description": "Filter logs by operation status (SUCCESS, FAILED)",
1041
+ "enum": ["SUCCESS", "FAILED"]
1042
+ },
1043
+ "limit": {
1044
+ "type": "integer",
1045
+ "description": "Maximum number of logs to return",
1046
+ "default": 100
1047
+ }
1048
+ },
1049
+ "required": [],
1050
+ },
1051
+ ),
668
1052
  ]
669
1053
 
670
1054
  async def _handle_list_connections(
@@ -932,6 +1316,110 @@ class ConnectionServer:
932
1316
 
933
1317
  return [types.TextContent(type="text", text="\n".join(analysis))]
934
1318
 
1319
+ async def _handle_execute_write(
1320
+ self, connection: str, sql: str, confirmation: str
1321
+ ) -> list[types.TextContent]:
1322
+ """处理执行写操作工具调用
1323
+
1324
+ Args:
1325
+ connection: 数据库连接名称
1326
+ sql: SQL写操作语句
1327
+ confirmation: 确认字符串
1328
+
1329
+ Returns:
1330
+ list[types.TextContent]: 执行结果
1331
+
1332
+ Raises:
1333
+ ConfigurationError: 如果SQL为空、确认字符串不正确、连接不可写或没有表级权限
1334
+ """
1335
+ if not sql:
1336
+ raise ConfigurationError(EMPTY_QUERY_ERROR)
1337
+
1338
+ # 验证确认字符串
1339
+ if confirmation != "CONFIRM_WRITE":
1340
+ raise ConfigurationError(WRITE_CONFIRMATION_REQUIRED_ERROR)
1341
+
1342
+ # 获取SQL类型和表名
1343
+ sql_type = self._get_sql_type(sql.strip())
1344
+ if sql_type not in ["INSERT", "UPDATE", "DELETE"]:
1345
+ raise ConfigurationError(UNSUPPORTED_WRITE_OPERATION_ERROR.format(operation=sql_type))
1346
+
1347
+ table_name = self._extract_table_name(sql)
1348
+
1349
+ # 获取连接配置并验证写权限
1350
+ db_config = self._get_config_or_raise(connection)
1351
+ await self._check_write_permission(connection, table_name, sql_type)
1352
+
1353
+ # 执行写操作
1354
+ async with self.get_handler(connection) as handler:
1355
+ self.send_log(
1356
+ LOG_LEVEL_NOTICE,
1357
+ f"Executing write operation: {sql_type} on table {table_name} in connection {connection}",
1358
+ )
1359
+
1360
+ try:
1361
+ result = await handler.execute_write_query(sql)
1362
+ self.send_log(
1363
+ LOG_LEVEL_INFO,
1364
+ f"Write operation executed successfully: {sql_type} on table {table_name}",
1365
+ )
1366
+ return [types.TextContent(type="text", text=result)]
1367
+ except Exception as e:
1368
+ self.send_log(
1369
+ LOG_LEVEL_ERROR,
1370
+ f"Write operation failed: {str(e)}",
1371
+ )
1372
+ raise
1373
+
1374
+ async def _handle_get_audit_logs(
1375
+ self,
1376
+ connection: str = None,
1377
+ table: str = None,
1378
+ operation_type: str = None,
1379
+ status: str = None,
1380
+ limit: int = 100
1381
+ ) -> list[types.TextContent]:
1382
+ """处理获取审计日志工具调用
1383
+
1384
+ Args:
1385
+ connection: 数据库连接名称(可选)
1386
+ table: 表名(可选)
1387
+ operation_type: 操作类型(可选,INSERT/UPDATE/DELETE)
1388
+ status: 操作状态(可选,SUCCESS/FAILED)
1389
+ limit: 返回记录数量限制
1390
+
1391
+ Returns:
1392
+ list[types.TextContent]: 审计日志
1393
+ """
1394
+ # 获取审计日志
1395
+ logs = get_logs(
1396
+ connection_name=connection,
1397
+ table_name=table,
1398
+ operation_type=operation_type,
1399
+ status=status,
1400
+ limit=limit
1401
+ )
1402
+
1403
+ # 格式化日志
1404
+ formatted_logs = format_logs(logs)
1405
+
1406
+ # 添加过滤条件信息
1407
+ filter_info = []
1408
+ if connection:
1409
+ filter_info.append(f"Connection: {connection}")
1410
+ if table:
1411
+ filter_info.append(f"Table: {table}")
1412
+ if operation_type:
1413
+ filter_info.append(f"Operation: {operation_type}")
1414
+ if status:
1415
+ filter_info.append(f"Status: {status}")
1416
+
1417
+ if filter_info:
1418
+ filter_text = "Filters applied: " + ", ".join(filter_info)
1419
+ formatted_logs = f"{filter_text}\n\n{formatted_logs}"
1420
+
1421
+ return [types.TextContent(type="text", text=formatted_logs)]
1422
+
935
1423
  def _get_optimization_suggestions(
936
1424
  self, explain_result: str, duration: float
937
1425
  ) -> list[str]:
@@ -1028,6 +1516,16 @@ class ConnectionServer:
1028
1516
  elif name == "dbutils-analyze-query":
1029
1517
  sql = arguments.get("sql", "").strip()
1030
1518
  return await self._handle_analyze_query(connection, sql)
1519
+ elif name == "dbutils-execute-write":
1520
+ sql = arguments.get("sql", "").strip()
1521
+ confirmation = arguments.get("confirmation", "").strip()
1522
+ return await self._handle_execute_write(connection, sql, confirmation)
1523
+ elif name == "dbutils-get-audit-logs":
1524
+ table = arguments.get("table", "").strip()
1525
+ operation_type = arguments.get("operation_type", "").strip()
1526
+ status = arguments.get("status", "").strip()
1527
+ limit = arguments.get("limit", 100)
1528
+ return await self._handle_get_audit_logs(connection, table, operation_type, status, limit)
1031
1529
  else:
1032
1530
  raise ConfigurationError(f"Unknown tool: {name}")
1033
1531
 
mcp_dbutils/config.py CHANGED
@@ -2,18 +2,111 @@
2
2
 
3
3
  import os
4
4
  from abc import ABC, abstractmethod
5
- from typing import Any, Dict, Literal
5
+ from typing import Any, Dict, List, Literal, Optional, Set, Union
6
6
 
7
7
  import yaml
8
8
 
9
9
  # Supported connection types
10
10
  ConnectionType = Literal['sqlite', 'postgres', 'mysql']
11
11
 
12
+ # Supported write operations
13
+ WriteOperationType = Literal['INSERT', 'UPDATE', 'DELETE']
14
+
15
+ # Default policy for tables not explicitly listed in write_permissions
16
+ DefaultPolicyType = Literal['read_only', 'allow_all']
17
+
18
+ class WritePermissions:
19
+ """Write permissions configuration"""
20
+
21
+ def __init__(self, config: Optional[Dict[str, Any]] = None):
22
+ """Initialize write permissions
23
+
24
+ Args:
25
+ config: Write permissions configuration dictionary
26
+ """
27
+ self.tables: Dict[str, Set[WriteOperationType]] = {}
28
+ self.default_policy: DefaultPolicyType = 'read_only'
29
+
30
+ if config:
31
+ # Parse table permissions
32
+ if 'tables' in config and isinstance(config['tables'], dict):
33
+ for table_name, table_config in config['tables'].items():
34
+ operations: Set[WriteOperationType] = set()
35
+
36
+ if isinstance(table_config, dict) and 'operations' in table_config:
37
+ ops = table_config['operations']
38
+ if isinstance(ops, list):
39
+ for op in ops:
40
+ if op in ('INSERT', 'UPDATE', 'DELETE'):
41
+ operations.add(op) # type: ignore
42
+
43
+ # If no operations specified, allow all
44
+ if not operations:
45
+ operations = {'INSERT', 'UPDATE', 'DELETE'} # type: ignore
46
+
47
+ self.tables[table_name] = operations
48
+
49
+ # Parse default policy
50
+ if 'default_policy' in config:
51
+ policy = config['default_policy']
52
+ if policy in ('read_only', 'allow_all'):
53
+ self.default_policy = policy # type: ignore
54
+
55
+ def can_write_to_table(self, table_name: str) -> bool:
56
+ """Check if writing to the table is allowed
57
+
58
+ Args:
59
+ table_name: Name of the table
60
+
61
+ Returns:
62
+ True if writing to the table is allowed, False otherwise
63
+ """
64
+ # If table is explicitly listed, it's writable
65
+ if table_name in self.tables:
66
+ return True
67
+
68
+ # Otherwise, check default policy
69
+ return self.default_policy == 'allow_all'
70
+
71
+ def allowed_operations(self, table_name: str) -> Set[WriteOperationType]:
72
+ """Get allowed operations for a table
73
+
74
+ Args:
75
+ table_name: Name of the table
76
+
77
+ Returns:
78
+ Set of allowed operations
79
+ """
80
+ # If table is explicitly listed, return its allowed operations
81
+ if table_name in self.tables:
82
+ return self.tables[table_name]
83
+
84
+ # Otherwise, check default policy
85
+ if self.default_policy == 'allow_all':
86
+ return {'INSERT', 'UPDATE', 'DELETE'} # type: ignore
87
+
88
+ # Default to empty set (no operations allowed)
89
+ return set() # type: ignore
90
+
91
+ def is_operation_allowed(self, table_name: str, operation: WriteOperationType) -> bool:
92
+ """Check if an operation is allowed on a table
93
+
94
+ Args:
95
+ table_name: Name of the table
96
+ operation: Operation type
97
+
98
+ Returns:
99
+ True if the operation is allowed, False otherwise
100
+ """
101
+ return operation in self.allowed_operations(table_name)
102
+
12
103
  class ConnectionConfig(ABC):
13
104
  """Base class for connection configuration"""
14
105
 
15
106
  debug: bool = False
16
107
  type: ConnectionType # Connection type
108
+ writable: bool = False # Whether write operations are allowed
109
+ write_permissions: Optional[WritePermissions] = None # Write permissions configuration
17
110
 
18
111
  @abstractmethod
19
112
  def get_connection_params(self) -> Dict[str, Any]:
@@ -50,6 +143,15 @@ class ConnectionConfig(ABC):
50
143
  if db_type not in ('sqlite', 'postgres', 'mysql'):
51
144
  raise ValueError(f"Invalid type value in database configuration {conn_name}: {db_type}")
52
145
 
146
+ # Validate write permissions if writable is true
147
+ if db_config.get('writable', False):
148
+ if not isinstance(db_config.get('writable'), bool):
149
+ raise ValueError(f"Invalid writable value in database configuration {conn_name}: {db_config['writable']}")
150
+
151
+ # Validate write_permissions if present
152
+ if 'write_permissions' in db_config and not isinstance(db_config['write_permissions'], dict):
153
+ raise ValueError(f"Invalid write_permissions in database configuration {conn_name}: {db_config['write_permissions']}")
154
+
53
155
  return connections
54
156
 
55
157
  @classmethod