Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

volume_permissions.py 26KB

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