|
63 | 63 | from sqlmesh.core.engine_adapter._typing import (
|
64 | 64 | DF,
|
65 | 65 | BigframeSession,
|
| 66 | + GrantsConfig, |
66 | 67 | PySparkDataFrame,
|
67 | 68 | PySparkSession,
|
68 | 69 | Query,
|
|
79 | 80 | KEY_FOR_CREATABLE_TYPE = "CREATABLE_TYPE"
|
80 | 81 |
|
81 | 82 |
|
| 83 | +# Use existing DataObjectType from shared module for grants |
| 84 | + |
| 85 | + |
82 | 86 | @set_catalog()
|
83 | 87 | class EngineAdapter:
|
84 | 88 | """Base class wrapping a Database API compliant connection.
|
@@ -114,6 +118,7 @@ class EngineAdapter:
|
114 | 118 | SUPPORTS_TUPLE_IN = True
|
115 | 119 | HAS_VIEW_BINDING = False
|
116 | 120 | SUPPORTS_REPLACE_TABLE = True
|
| 121 | + SUPPORTS_GRANTS = False |
117 | 122 | DEFAULT_CATALOG_TYPE = DIALECT
|
118 | 123 | QUOTE_IDENTIFIERS_IN_VIEWS = True
|
119 | 124 | MAX_IDENTIFIER_LENGTH: t.Optional[int] = None
|
@@ -2345,6 +2350,193 @@ def wap_publish(self, table_name: TableName, wap_id: str) -> None:
|
2345 | 2350 | """
|
2346 | 2351 | raise NotImplementedError(f"Engine does not support WAP: {type(self)}")
|
2347 | 2352 |
|
| 2353 | + def _get_current_grants_config(self, table: exp.Table) -> GrantsConfig: |
| 2354 | + """Returns current grants for a table as a dictionary. |
| 2355 | +
|
| 2356 | + This method queries the database and returns the current grants/permissions |
| 2357 | + for the given table, parsed into a dictionary format. The it handles |
| 2358 | + case-insensitive comparison between these current grants and the desired |
| 2359 | + grants from model configuration. |
| 2360 | +
|
| 2361 | + Args: |
| 2362 | + table: The table/view to query grants for. |
| 2363 | +
|
| 2364 | + Returns: |
| 2365 | + Dictionary mapping permissions to lists of grantees. Permission names |
| 2366 | + should be returned as the database provides them (typically uppercase |
| 2367 | + for standard SQL permissions, but engine-specific roles may vary). |
| 2368 | +
|
| 2369 | + Raises: |
| 2370 | + NotImplementedError: If the engine does not support grants. |
| 2371 | + """ |
| 2372 | + if not self.SUPPORTS_GRANTS: |
| 2373 | + raise NotImplementedError(f"Engine does not support grants: {type(self)}") |
| 2374 | + raise NotImplementedError("Subclass must implement get_current_grants") |
| 2375 | + |
| 2376 | + def _sync_grants_config( |
| 2377 | + self, |
| 2378 | + table: exp.Table, |
| 2379 | + grants_config: GrantsConfig, |
| 2380 | + table_type: DataObjectType = DataObjectType.TABLE, |
| 2381 | + ) -> None: |
| 2382 | + """Applies the grants_config to a table authoritatively. |
| 2383 | + It first compares the specified grants against the current grants, and then |
| 2384 | + applies the diffs to the table by revoking and granting privileges as needed. |
| 2385 | +
|
| 2386 | + Args: |
| 2387 | + table: The table/view to apply grants to. |
| 2388 | + grants_config: Dictionary mapping privileges to lists of grantees. |
| 2389 | + table_type: The type of database object (TABLE, VIEW, MATERIALIZED_VIEW). |
| 2390 | + """ |
| 2391 | + if not self.SUPPORTS_GRANTS: |
| 2392 | + raise NotImplementedError(f"Engine does not support grants: {type(self)}") |
| 2393 | + |
| 2394 | + current_grants = self._get_current_grants_config(table) |
| 2395 | + new_grants, revoked_grants = self._diff_grants_configs(grants_config, current_grants) |
| 2396 | + revoke_exprs = self._revoke_grants_config_expr(table, revoked_grants, table_type) |
| 2397 | + grant_exprs = self._apply_grants_config_expr(table, new_grants, table_type) |
| 2398 | + dcl_exprs = revoke_exprs + grant_exprs |
| 2399 | + |
| 2400 | + if dcl_exprs: |
| 2401 | + self.execute(dcl_exprs) |
| 2402 | + |
| 2403 | + def _apply_grants_config( |
| 2404 | + self, |
| 2405 | + table: exp.Table, |
| 2406 | + grants_config: GrantsConfig, |
| 2407 | + table_type: DataObjectType = DataObjectType.TABLE, |
| 2408 | + ) -> None: |
| 2409 | + """Applies grants to a table. |
| 2410 | +
|
| 2411 | + Args: |
| 2412 | + table: The table/view to grant permissions on. |
| 2413 | + grants_config: Dictionary mapping privileges to lists of grantees. |
| 2414 | + table_type: The type of database object (TABLE, VIEW, MATERIALIZED_VIEW). |
| 2415 | +
|
| 2416 | + Raises: |
| 2417 | + NotImplementedError: If the engine does not support grants. |
| 2418 | + """ |
| 2419 | + |
| 2420 | + if grants := self._apply_grants_config_expr(table, grants_config, table_type): |
| 2421 | + self.execute(grants) |
| 2422 | + |
| 2423 | + def _revoke_grants_config( |
| 2424 | + self, |
| 2425 | + table: exp.Table, |
| 2426 | + grants_config: GrantsConfig, |
| 2427 | + table_type: DataObjectType = DataObjectType.TABLE, |
| 2428 | + ) -> None: |
| 2429 | + """Revokes grants from a table. |
| 2430 | +
|
| 2431 | + Args: |
| 2432 | + table: The table/view to revoke privileges from. |
| 2433 | + grants_config: Dictionary mapping privileges to lists of grantees. |
| 2434 | + table_type: The type of database object (TABLE, VIEW, MATERIALIZED_VIEW). |
| 2435 | +
|
| 2436 | + Raises: |
| 2437 | + NotImplementedError: If the engine does not support grants. |
| 2438 | + """ |
| 2439 | + if revokes := self._revoke_grants_config_expr(table, grants_config, table_type): |
| 2440 | + self.execute(revokes) |
| 2441 | + |
| 2442 | + def _apply_grants_config_expr( |
| 2443 | + self, |
| 2444 | + table: exp.Table, |
| 2445 | + grant_config: GrantsConfig, |
| 2446 | + table_type: DataObjectType = DataObjectType.TABLE, |
| 2447 | + ) -> t.List[exp.Grant]: |
| 2448 | + """Returns SQLGlot Grant expressions to apply grants to a table. |
| 2449 | +
|
| 2450 | + Args: |
| 2451 | + table: The table/view to grant permissions on. |
| 2452 | + grant_config: Dictionary mapping permissions to lists of grantees. |
| 2453 | + table_type: The type of database object (TABLE, VIEW, MATERIALIZED_VIEW). |
| 2454 | +
|
| 2455 | + Returns: |
| 2456 | + List of SQLGlot Grant expressions. |
| 2457 | +
|
| 2458 | + Raises: |
| 2459 | + NotImplementedError: If the engine does not support grants. |
| 2460 | + """ |
| 2461 | + if not self.SUPPORTS_GRANTS: |
| 2462 | + raise NotImplementedError(f"Engine does not support grants: {type(self)}") |
| 2463 | + raise NotImplementedError("Subclass must implement _apply_grants_config_expr") |
| 2464 | + |
| 2465 | + def _revoke_grants_config_expr( |
| 2466 | + self, |
| 2467 | + table: exp.Table, |
| 2468 | + grant_config: GrantsConfig, |
| 2469 | + table_type: DataObjectType = DataObjectType.TABLE, |
| 2470 | + ) -> t.List[exp.Expression]: |
| 2471 | + """Returns SQLGlot expressions to revoke grants from a table. |
| 2472 | +
|
| 2473 | + Note: SQLGlot doesn't yet have a Revoke expression type, so implementations |
| 2474 | + may return other expression types or handle revokes as strings. |
| 2475 | +
|
| 2476 | + Args: |
| 2477 | + table: The table/view to revoke permissions from. |
| 2478 | + grant_config: Dictionary mapping permissions to lists of grantees. |
| 2479 | + table_type: The type of database object (TABLE, VIEW, MATERIALIZED_VIEW). |
| 2480 | +
|
| 2481 | + Returns: |
| 2482 | + List of SQLGlot expressions for revoke operations. |
| 2483 | +
|
| 2484 | + Raises: |
| 2485 | + NotImplementedError: If the engine does not support grants. |
| 2486 | + """ |
| 2487 | + if not self.SUPPORTS_GRANTS: |
| 2488 | + raise NotImplementedError(f"Engine does not support grants: {type(self)}") |
| 2489 | + raise NotImplementedError("Subclass must implement _revoke_grants_config_expr") |
| 2490 | + |
| 2491 | + @classmethod |
| 2492 | + def _diff_grants_configs( |
| 2493 | + cls, new_config: GrantsConfig, old_config: GrantsConfig |
| 2494 | + ) -> t.Tuple[GrantsConfig, GrantsConfig]: |
| 2495 | + """Compute additions and removals between two grants configurations. |
| 2496 | +
|
| 2497 | + This method compares new (desired) and old (current) GrantsConfigs case-insensitively |
| 2498 | + for both privilege keys and grantees, while preserving original casing |
| 2499 | + in the output GrantsConfigs. |
| 2500 | +
|
| 2501 | + Args: |
| 2502 | + new_config: Desired grants configuration (specified by the user). |
| 2503 | + old_config: Current grants configuration (returned by the database). |
| 2504 | +
|
| 2505 | + Returns: |
| 2506 | + A tuple of (additions, removals) GrantsConfig where: |
| 2507 | + - additions contains privileges/grantees present in new_config but not in old_config |
| 2508 | + - additions uses keys and grantee strings from new_config (user-specified casing) |
| 2509 | + - removals contains privileges/grantees present in old_config but not in new_config |
| 2510 | + - removals uses keys and grantee strings from old_config (database-returned casing) |
| 2511 | +
|
| 2512 | + Notes: |
| 2513 | + - Comparison is case-insensitive using casefold(); original casing is preserved in results. |
| 2514 | + - Overlapping grantees (case-insensitive) are excluded from the results. |
| 2515 | + """ |
| 2516 | + |
| 2517 | + def _diffs(config1: GrantsConfig, config2: GrantsConfig) -> GrantsConfig: |
| 2518 | + diffs: GrantsConfig = {} |
| 2519 | + cf_config2 = {k.casefold(): {g.casefold() for g in v} for k, v in config2.items()} |
| 2520 | + for key, grantees in config1.items(): |
| 2521 | + cf_key = key.casefold() |
| 2522 | + |
| 2523 | + # Missing key (add all grantees) |
| 2524 | + if cf_key not in cf_config2: |
| 2525 | + diffs[key] = grantees.copy() |
| 2526 | + continue |
| 2527 | + |
| 2528 | + # Include only grantees not in config2 |
| 2529 | + cf_grantees2 = cf_config2[cf_key] |
| 2530 | + diff_grantees = [] |
| 2531 | + for grantee in grantees: |
| 2532 | + if grantee.casefold() not in cf_grantees2: |
| 2533 | + diff_grantees.append(grantee) |
| 2534 | + if diff_grantees: |
| 2535 | + diffs[key] = diff_grantees |
| 2536 | + return diffs |
| 2537 | + |
| 2538 | + return _diffs(new_config, old_config), _diffs(old_config, new_config) |
| 2539 | + |
2348 | 2540 | @contextlib.contextmanager
|
2349 | 2541 | def transaction(
|
2350 | 2542 | self,
|
|
0 commit comments