You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

volume_permissions.py 26KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646
  1. """ClickZetta Volume permission management mechanism
  2. This module provides Volume permission checking, validation and management features.
  3. According to ClickZetta's permission model, different Volume types have different permission requirements.
  4. """
  5. import logging
  6. from enum import Enum
  7. from typing import Optional
  8. logger = logging.getLogger(__name__)
  9. class VolumePermission(Enum):
  10. """Volume permission type enumeration"""
  11. READ = "SELECT" # Corresponds to ClickZetta's SELECT permission
  12. WRITE = "INSERT,UPDATE,DELETE" # Corresponds to ClickZetta's write permissions
  13. LIST = "SELECT" # Listing files requires SELECT permission
  14. DELETE = "INSERT,UPDATE,DELETE" # Deleting files requires write permissions
  15. USAGE = "USAGE" # Basic permission required for External Volume
  16. class VolumePermissionManager:
  17. """Volume permission manager"""
  18. def __init__(self, connection_or_config, volume_type: str | None = None, volume_name: Optional[str] = None):
  19. """Initialize permission manager
  20. Args:
  21. connection_or_config: ClickZetta connection object or configuration dictionary
  22. volume_type: Volume type (user|table|external)
  23. volume_name: Volume name (for external volume)
  24. """
  25. # Support two initialization methods: connection object or configuration dictionary
  26. if isinstance(connection_or_config, dict):
  27. # Create connection from configuration dictionary
  28. import clickzetta # type: ignore[import-untyped]
  29. config = connection_or_config
  30. self._connection = clickzetta.connect(
  31. username=config.get("username"),
  32. password=config.get("password"),
  33. instance=config.get("instance"),
  34. service=config.get("service"),
  35. workspace=config.get("workspace"),
  36. vcluster=config.get("vcluster"),
  37. schema=config.get("schema") or config.get("database"),
  38. )
  39. self._volume_type = config.get("volume_type", volume_type)
  40. self._volume_name = config.get("volume_name", volume_name)
  41. else:
  42. # Use connection object directly
  43. self._connection = connection_or_config
  44. self._volume_type = volume_type
  45. self._volume_name = volume_name
  46. if not self._connection:
  47. raise ValueError("Valid connection or config is required")
  48. if not self._volume_type:
  49. raise ValueError("volume_type is required")
  50. self._permission_cache: dict[str, set[str]] = {}
  51. self._current_username = None # Will get current username from connection
  52. def check_permission(self, operation: VolumePermission, dataset_id: Optional[str] = None) -> bool:
  53. """Check if user has permission to perform specific operation
  54. Args:
  55. operation: Type of operation to perform
  56. dataset_id: Dataset ID (for table volume)
  57. Returns:
  58. True if user has permission, False otherwise
  59. """
  60. try:
  61. if self._volume_type == "user":
  62. return self._check_user_volume_permission(operation)
  63. elif self._volume_type == "table":
  64. return self._check_table_volume_permission(operation, dataset_id)
  65. elif self._volume_type == "external":
  66. return self._check_external_volume_permission(operation)
  67. else:
  68. logger.warning("Unknown volume type: %s", self._volume_type)
  69. return False
  70. except Exception as e:
  71. logger.exception("Permission check failed")
  72. return False
  73. def _check_user_volume_permission(self, operation: VolumePermission) -> bool:
  74. """Check User Volume permission
  75. User Volume permission rules:
  76. - User has full permissions on their own User Volume
  77. - As long as user can connect to ClickZetta, they have basic User Volume permissions by default
  78. - Focus more on connection authentication rather than complex permission checking
  79. """
  80. try:
  81. # Get current username
  82. current_user = self._get_current_username()
  83. # Check basic connection status
  84. with self._connection.cursor() as cursor:
  85. # Simple connection test, if query can be executed user has basic permissions
  86. cursor.execute("SELECT 1")
  87. result = cursor.fetchone()
  88. if result:
  89. logger.debug(
  90. "User Volume permission check for %s, operation %s: granted (basic connection verified)",
  91. current_user,
  92. operation.name,
  93. )
  94. return True
  95. else:
  96. logger.warning(
  97. "User Volume permission check failed: cannot verify basic connection for %s", current_user
  98. )
  99. return False
  100. except Exception as e:
  101. logger.exception("User Volume permission check failed")
  102. # For User Volume, if permission check fails, it might be a configuration issue, provide friendlier error message
  103. logger.info("User Volume permission check failed, but permission checking is disabled in this version")
  104. return False
  105. def _check_table_volume_permission(self, operation: VolumePermission, dataset_id: Optional[str]) -> bool:
  106. """Check Table Volume permission
  107. Table Volume permission rules:
  108. - Table Volume permissions inherit from corresponding table permissions
  109. - SELECT permission -> can READ/LIST files
  110. - INSERT,UPDATE,DELETE permissions -> can WRITE/DELETE files
  111. """
  112. if not dataset_id:
  113. logger.warning("dataset_id is required for table volume permission check")
  114. return False
  115. table_name = f"dataset_{dataset_id}" if not dataset_id.startswith("dataset_") else dataset_id
  116. try:
  117. # Check table permissions
  118. permissions = self._get_table_permissions(table_name)
  119. required_permissions = set(operation.value.split(","))
  120. # Check if has all required permissions
  121. has_permission = required_permissions.issubset(permissions)
  122. logger.debug(
  123. "Table Volume permission check for %s, operation %s: required=%s, has=%s, granted=%s",
  124. table_name,
  125. operation.name,
  126. required_permissions,
  127. permissions,
  128. has_permission,
  129. )
  130. return has_permission
  131. except Exception as e:
  132. logger.exception("Table volume permission check failed for %s", table_name)
  133. return False
  134. def _check_external_volume_permission(self, operation: VolumePermission) -> bool:
  135. """Check External Volume permission
  136. External Volume permission rules:
  137. - Try to get permissions for External Volume
  138. - If permission check fails, perform fallback verification
  139. - For development environment, provide more lenient permission checking
  140. """
  141. if not self._volume_name:
  142. logger.warning("volume_name is required for external volume permission check")
  143. return False
  144. try:
  145. # Check External Volume permissions
  146. permissions = self._get_external_volume_permissions(self._volume_name)
  147. # External Volume permission mapping: determine required permissions based on operation type
  148. required_permissions = set()
  149. if operation in [VolumePermission.READ, VolumePermission.LIST]:
  150. required_permissions.add("read")
  151. elif operation in [VolumePermission.WRITE, VolumePermission.DELETE]:
  152. required_permissions.add("write")
  153. # Check if has all required permissions
  154. has_permission = required_permissions.issubset(permissions)
  155. logger.debug(
  156. "External Volume permission check for %s, operation %s: required=%s, has=%s, granted=%s",
  157. self._volume_name,
  158. operation.name,
  159. required_permissions,
  160. permissions,
  161. has_permission,
  162. )
  163. # If permission check fails, try fallback verification
  164. if not has_permission:
  165. logger.info("Direct permission check failed for %s, trying fallback verification", self._volume_name)
  166. # Fallback verification: try listing Volume to verify basic access permissions
  167. try:
  168. with self._connection.cursor() as cursor:
  169. cursor.execute("SHOW VOLUMES")
  170. volumes = cursor.fetchall()
  171. for volume in volumes:
  172. if len(volume) > 0 and volume[0] == self._volume_name:
  173. logger.info("Fallback verification successful for %s", self._volume_name)
  174. return True
  175. except Exception as fallback_e:
  176. logger.warning("Fallback verification failed for %s: %s", self._volume_name, fallback_e)
  177. return has_permission
  178. except Exception as e:
  179. logger.exception("External volume permission check failed for %s", self._volume_name)
  180. logger.info("External Volume permission check failed, but permission checking is disabled in this version")
  181. return False
  182. def _get_table_permissions(self, table_name: str) -> set[str]:
  183. """Get user permissions for specified table
  184. Args:
  185. table_name: Table name
  186. Returns:
  187. Set of user permissions for this table
  188. """
  189. cache_key = f"table:{table_name}"
  190. if cache_key in self._permission_cache:
  191. return self._permission_cache[cache_key]
  192. permissions = set()
  193. try:
  194. with self._connection.cursor() as cursor:
  195. # Use correct ClickZetta syntax to check current user permissions
  196. cursor.execute("SHOW GRANTS")
  197. grants = cursor.fetchall()
  198. # Parse permission results, find permissions for this table
  199. for grant in grants:
  200. if len(grant) >= 3: # Typical format: (privilege, object_type, object_name, ...)
  201. privilege = grant[0].upper()
  202. object_type = grant[1].upper() if len(grant) > 1 else ""
  203. object_name = grant[2] if len(grant) > 2 else ""
  204. # Check if it's permission for this table
  205. if (
  206. object_type == "TABLE"
  207. and object_name == table_name
  208. or object_type == "SCHEMA"
  209. and object_name in table_name
  210. ):
  211. if privilege in ["SELECT", "INSERT", "UPDATE", "DELETE", "ALL"]:
  212. if privilege == "ALL":
  213. permissions.update(["SELECT", "INSERT", "UPDATE", "DELETE"])
  214. else:
  215. permissions.add(privilege)
  216. # If no explicit permissions found, try executing a simple query to verify permissions
  217. if not permissions:
  218. try:
  219. cursor.execute(f"SELECT COUNT(*) FROM {table_name} LIMIT 1")
  220. permissions.add("SELECT")
  221. except Exception:
  222. logger.debug("Cannot query table %s, no SELECT permission", table_name)
  223. except Exception as e:
  224. logger.warning("Could not check table permissions for %s: %s", table_name, e)
  225. # Safe default: deny access when permission check fails
  226. pass
  227. # Cache permission information
  228. self._permission_cache[cache_key] = permissions
  229. return permissions
  230. def _get_current_username(self) -> str:
  231. """Get current username"""
  232. if self._current_username:
  233. return self._current_username
  234. try:
  235. with self._connection.cursor() as cursor:
  236. cursor.execute("SELECT CURRENT_USER()")
  237. result = cursor.fetchone()
  238. if result:
  239. self._current_username = result[0]
  240. return str(self._current_username)
  241. except Exception as e:
  242. logger.exception("Failed to get current username")
  243. return "unknown"
  244. def _get_user_permissions(self, username: str) -> set[str]:
  245. """Get user's basic permission set"""
  246. cache_key = f"user_permissions:{username}"
  247. if cache_key in self._permission_cache:
  248. return self._permission_cache[cache_key]
  249. permissions = set()
  250. try:
  251. with self._connection.cursor() as cursor:
  252. # Use correct ClickZetta syntax to check current user permissions
  253. cursor.execute("SHOW GRANTS")
  254. grants = cursor.fetchall()
  255. # Parse permission results, find user's basic permissions
  256. for grant in grants:
  257. if len(grant) >= 3: # Typical format: (privilege, object_type, object_name, ...)
  258. privilege = grant[0].upper()
  259. object_type = grant[1].upper() if len(grant) > 1 else ""
  260. # Collect all relevant permissions
  261. if privilege in ["SELECT", "INSERT", "UPDATE", "DELETE", "ALL"]:
  262. if privilege == "ALL":
  263. permissions.update(["SELECT", "INSERT", "UPDATE", "DELETE"])
  264. else:
  265. permissions.add(privilege)
  266. except Exception as e:
  267. logger.warning("Could not check user permissions for %s: %s", username, e)
  268. # Safe default: deny access when permission check fails
  269. pass
  270. # Cache permission information
  271. self._permission_cache[cache_key] = permissions
  272. return permissions
  273. def _get_external_volume_permissions(self, volume_name: str) -> set[str]:
  274. """Get user permissions for specified External Volume
  275. Args:
  276. volume_name: External Volume name
  277. Returns:
  278. Set of user permissions for this Volume
  279. """
  280. cache_key = f"external_volume:{volume_name}"
  281. if cache_key in self._permission_cache:
  282. return self._permission_cache[cache_key]
  283. permissions = set()
  284. try:
  285. with self._connection.cursor() as cursor:
  286. # Use correct ClickZetta syntax to check Volume permissions
  287. logger.info("Checking permissions for volume: %s", volume_name)
  288. cursor.execute(f"SHOW GRANTS ON VOLUME {volume_name}")
  289. grants = cursor.fetchall()
  290. logger.info("Raw grants result for %s: %s", volume_name, grants)
  291. # Parse permission results
  292. # Format: (granted_type, privilege, conditions, granted_on, object_name, granted_to,
  293. # grantee_name, grantor_name, grant_option, granted_time)
  294. for grant in grants:
  295. logger.info("Processing grant: %s", grant)
  296. if len(grant) >= 5:
  297. granted_type = grant[0]
  298. privilege = grant[1].upper()
  299. granted_on = grant[3]
  300. object_name = grant[4]
  301. logger.info(
  302. "Grant details - type: %s, privilege: %s, granted_on: %s, object_name: %s",
  303. granted_type,
  304. privilege,
  305. granted_on,
  306. object_name,
  307. )
  308. # Check if it's permission for this Volume or hierarchical permission
  309. if (
  310. granted_type == "PRIVILEGE" and granted_on == "VOLUME" and object_name.endswith(volume_name)
  311. ) or (granted_type == "OBJECT_HIERARCHY" and granted_on == "VOLUME"):
  312. logger.info("Matching grant found for %s", volume_name)
  313. if "READ" in privilege:
  314. permissions.add("read")
  315. logger.info("Added READ permission for %s", volume_name)
  316. if "WRITE" in privilege:
  317. permissions.add("write")
  318. logger.info("Added WRITE permission for %s", volume_name)
  319. if "ALTER" in privilege:
  320. permissions.add("alter")
  321. logger.info("Added ALTER permission for %s", volume_name)
  322. if privilege == "ALL":
  323. permissions.update(["read", "write", "alter"])
  324. logger.info("Added ALL permissions for %s", volume_name)
  325. logger.info("Final permissions for %s: %s", volume_name, permissions)
  326. # If no explicit permissions found, try viewing Volume list to verify basic permissions
  327. if not permissions:
  328. try:
  329. cursor.execute("SHOW VOLUMES")
  330. volumes = cursor.fetchall()
  331. for volume in volumes:
  332. if len(volume) > 0 and volume[0] == volume_name:
  333. permissions.add("read") # At least has read permission
  334. logger.debug("Volume %s found in SHOW VOLUMES, assuming read permission", volume_name)
  335. break
  336. except Exception:
  337. logger.debug("Cannot access volume %s, no basic permission", volume_name)
  338. except Exception as e:
  339. logger.warning("Could not check external volume permissions for %s: %s", volume_name, e)
  340. # When permission check fails, try basic Volume access verification
  341. try:
  342. with self._connection.cursor() as cursor:
  343. cursor.execute("SHOW VOLUMES")
  344. volumes = cursor.fetchall()
  345. for volume in volumes:
  346. if len(volume) > 0 and volume[0] == volume_name:
  347. logger.info("Basic volume access verified for %s", volume_name)
  348. permissions.add("read")
  349. permissions.add("write") # Assume has write permission
  350. break
  351. except Exception as basic_e:
  352. logger.warning("Basic volume access check failed for %s: %s", volume_name, basic_e)
  353. # Last fallback: assume basic permissions
  354. permissions.add("read")
  355. # Cache permission information
  356. self._permission_cache[cache_key] = permissions
  357. return permissions
  358. def clear_permission_cache(self):
  359. """Clear permission cache"""
  360. self._permission_cache.clear()
  361. logger.debug("Permission cache cleared")
  362. def get_permission_summary(self, dataset_id: Optional[str] = None) -> dict[str, bool]:
  363. """Get permission summary
  364. Args:
  365. dataset_id: Dataset ID (for table volume)
  366. Returns:
  367. Permission summary dictionary
  368. """
  369. summary = {}
  370. for operation in VolumePermission:
  371. summary[operation.name.lower()] = self.check_permission(operation, dataset_id)
  372. return summary
  373. def check_inherited_permission(self, file_path: str, operation: VolumePermission) -> bool:
  374. """Check permission inheritance for file path
  375. Args:
  376. file_path: File path
  377. operation: Operation to perform
  378. Returns:
  379. True if user has permission, False otherwise
  380. """
  381. try:
  382. # Parse file path
  383. path_parts = file_path.strip("/").split("/")
  384. if not path_parts:
  385. logger.warning("Invalid file path for permission inheritance check")
  386. return False
  387. # For Table Volume, first layer is dataset_id
  388. if self._volume_type == "table":
  389. if len(path_parts) < 1:
  390. return False
  391. dataset_id = path_parts[0]
  392. # Check permissions for dataset
  393. has_dataset_permission = self.check_permission(operation, dataset_id)
  394. if not has_dataset_permission:
  395. logger.debug("Permission denied for dataset %s", dataset_id)
  396. return False
  397. # Check path traversal attack
  398. if self._contains_path_traversal(file_path):
  399. logger.warning("Path traversal attack detected: %s", file_path)
  400. return False
  401. # Check if accessing sensitive directory
  402. if self._is_sensitive_path(file_path):
  403. logger.warning("Access to sensitive path denied: %s", file_path)
  404. return False
  405. logger.debug("Permission inherited for path %s", file_path)
  406. return True
  407. elif self._volume_type == "user":
  408. # User Volume permission inheritance
  409. current_user = self._get_current_username()
  410. # Check if attempting to access other user's directory
  411. if len(path_parts) > 1 and path_parts[0] != current_user:
  412. logger.warning("User %s attempted to access %s's directory", current_user, path_parts[0])
  413. return False
  414. # Check basic permissions
  415. return self.check_permission(operation)
  416. elif self._volume_type == "external":
  417. # External Volume permission inheritance
  418. # Check permissions for External Volume
  419. return self.check_permission(operation)
  420. else:
  421. logger.warning("Unknown volume type for permission inheritance: %s", self._volume_type)
  422. return False
  423. except Exception as e:
  424. logger.exception("Permission inheritance check failed")
  425. return False
  426. def _contains_path_traversal(self, file_path: str) -> bool:
  427. """Check if path contains path traversal attack"""
  428. # Check common path traversal patterns
  429. traversal_patterns = [
  430. "../",
  431. "..\\",
  432. "..%2f",
  433. "..%2F",
  434. "..%5c",
  435. "..%5C",
  436. "%2e%2e%2f",
  437. "%2e%2e%5c",
  438. "....//",
  439. "....\\\\",
  440. ]
  441. file_path_lower = file_path.lower()
  442. for pattern in traversal_patterns:
  443. if pattern in file_path_lower:
  444. return True
  445. # Check absolute path
  446. if file_path.startswith("/") or file_path.startswith("\\"):
  447. return True
  448. # Check Windows drive path
  449. if len(file_path) >= 2 and file_path[1] == ":":
  450. return True
  451. return False
  452. def _is_sensitive_path(self, file_path: str) -> bool:
  453. """Check if path is sensitive path"""
  454. sensitive_patterns = [
  455. "passwd",
  456. "shadow",
  457. "hosts",
  458. "config",
  459. "secrets",
  460. "private",
  461. "key",
  462. "certificate",
  463. "cert",
  464. "ssl",
  465. "database",
  466. "backup",
  467. "dump",
  468. "log",
  469. "tmp",
  470. ]
  471. file_path_lower = file_path.lower()
  472. return any(pattern in file_path_lower for pattern in sensitive_patterns)
  473. def validate_operation(self, operation: str, dataset_id: Optional[str] = None) -> bool:
  474. """Validate operation permission
  475. Args:
  476. operation: Operation name (save|load|exists|delete|scan)
  477. dataset_id: Dataset ID
  478. Returns:
  479. True if operation is allowed, False otherwise
  480. """
  481. operation_mapping = {
  482. "save": VolumePermission.WRITE,
  483. "load": VolumePermission.READ,
  484. "load_once": VolumePermission.READ,
  485. "load_stream": VolumePermission.READ,
  486. "download": VolumePermission.READ,
  487. "exists": VolumePermission.READ,
  488. "delete": VolumePermission.DELETE,
  489. "scan": VolumePermission.LIST,
  490. }
  491. if operation not in operation_mapping:
  492. logger.warning("Unknown operation: %s", operation)
  493. return False
  494. volume_permission = operation_mapping[operation]
  495. return self.check_permission(volume_permission, dataset_id)
  496. class VolumePermissionError(Exception):
  497. """Volume permission error exception"""
  498. def __init__(self, message: str, operation: str, volume_type: str, dataset_id: Optional[str] = None):
  499. self.operation = operation
  500. self.volume_type = volume_type
  501. self.dataset_id = dataset_id
  502. super().__init__(message)
  503. def check_volume_permission(
  504. permission_manager: VolumePermissionManager, operation: str, dataset_id: Optional[str] = None
  505. ) -> None:
  506. """Permission check decorator function
  507. Args:
  508. permission_manager: Permission manager
  509. operation: Operation name
  510. dataset_id: Dataset ID
  511. Raises:
  512. VolumePermissionError: If no permission
  513. """
  514. if not permission_manager.validate_operation(operation, dataset_id):
  515. error_message = f"Permission denied for operation '{operation}' on {permission_manager._volume_type} volume"
  516. if dataset_id:
  517. error_message += f" (dataset: {dataset_id})"
  518. raise VolumePermissionError(
  519. error_message,
  520. operation=operation,
  521. volume_type=permission_manager._volume_type or "unknown",
  522. dataset_id=dataset_id,
  523. )