|
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, |
@@ -114,6 +115,7 @@ class EngineAdapter: |
114 | 115 | SUPPORTS_TUPLE_IN = True |
115 | 116 | HAS_VIEW_BINDING = False |
116 | 117 | SUPPORTS_REPLACE_TABLE = True |
| 118 | + SUPPORTS_GRANTS = False |
117 | 119 | DEFAULT_CATALOG_TYPE = DIALECT |
118 | 120 | QUOTE_IDENTIFIERS_IN_VIEWS = True |
119 | 121 | MAX_IDENTIFIER_LENGTH: t.Optional[int] = None |
@@ -2478,6 +2480,33 @@ def wap_publish(self, table_name: TableName, wap_id: str) -> None: |
2478 | 2480 | """ |
2479 | 2481 | raise NotImplementedError(f"Engine does not support WAP: {type(self)}") |
2480 | 2482 |
|
| 2483 | + def sync_grants_config( |
| 2484 | + self, |
| 2485 | + table: exp.Table, |
| 2486 | + grants_config: GrantsConfig, |
| 2487 | + table_type: DataObjectType = DataObjectType.TABLE, |
| 2488 | + ) -> None: |
| 2489 | + """Applies the grants_config to a table authoritatively. |
| 2490 | + It first compares the specified grants against the current grants, and then |
| 2491 | + applies the diffs to the table by revoking and granting privileges as needed. |
| 2492 | +
|
| 2493 | + Args: |
| 2494 | + table: The table/view to apply grants to. |
| 2495 | + grants_config: Dictionary mapping privileges to lists of grantees. |
| 2496 | + table_type: The type of database object (TABLE, VIEW, MATERIALIZED_VIEW). |
| 2497 | + """ |
| 2498 | + if not self.SUPPORTS_GRANTS: |
| 2499 | + raise NotImplementedError(f"Engine does not support grants: {type(self)}") |
| 2500 | + |
| 2501 | + current_grants = self._get_current_grants_config(table) |
| 2502 | + new_grants, revoked_grants = self._diff_grants_configs(grants_config, current_grants) |
| 2503 | + revoke_exprs = self._revoke_grants_config_expr(table, revoked_grants, table_type) |
| 2504 | + grant_exprs = self._apply_grants_config_expr(table, new_grants, table_type) |
| 2505 | + dcl_exprs = revoke_exprs + grant_exprs |
| 2506 | + |
| 2507 | + if dcl_exprs: |
| 2508 | + self.execute(dcl_exprs) |
| 2509 | + |
2481 | 2510 | @contextlib.contextmanager |
2482 | 2511 | def transaction( |
2483 | 2512 | self, |
@@ -3029,6 +3058,124 @@ def _check_identifier_length(self, expression: exp.Expression) -> None: |
3029 | 3058 | def get_table_last_modified_ts(self, table_names: t.List[TableName]) -> t.List[int]: |
3030 | 3059 | raise NotImplementedError() |
3031 | 3060 |
|
| 3061 | + @classmethod |
| 3062 | + def _diff_grants_configs( |
| 3063 | + cls, new_config: GrantsConfig, old_config: GrantsConfig |
| 3064 | + ) -> t.Tuple[GrantsConfig, GrantsConfig]: |
| 3065 | + """Compute additions and removals between two grants configurations. |
| 3066 | +
|
| 3067 | + This method compares new (desired) and old (current) GrantsConfigs case-insensitively |
| 3068 | + for both privilege keys and grantees, while preserving original casing |
| 3069 | + in the output GrantsConfigs. |
| 3070 | +
|
| 3071 | + Args: |
| 3072 | + new_config: Desired grants configuration (specified by the user). |
| 3073 | + old_config: Current grants configuration (returned by the database). |
| 3074 | +
|
| 3075 | + Returns: |
| 3076 | + A tuple of (additions, removals) GrantsConfig where: |
| 3077 | + - additions contains privileges/grantees present in new_config but not in old_config |
| 3078 | + - additions uses keys and grantee strings from new_config (user-specified casing) |
| 3079 | + - removals contains privileges/grantees present in old_config but not in new_config |
| 3080 | + - removals uses keys and grantee strings from old_config (database-returned casing) |
| 3081 | +
|
| 3082 | + Notes: |
| 3083 | + - Comparison is case-insensitive using casefold(); original casing is preserved in results. |
| 3084 | + - Overlapping grantees (case-insensitive) are excluded from the results. |
| 3085 | + """ |
| 3086 | + |
| 3087 | + def _diffs(config1: GrantsConfig, config2: GrantsConfig) -> GrantsConfig: |
| 3088 | + diffs: GrantsConfig = {} |
| 3089 | + cf_config2 = {k.casefold(): {g.casefold() for g in v} for k, v in config2.items()} |
| 3090 | + for key, grantees in config1.items(): |
| 3091 | + cf_key = key.casefold() |
| 3092 | + |
| 3093 | + # Missing key (add all grantees) |
| 3094 | + if cf_key not in cf_config2: |
| 3095 | + diffs[key] = grantees.copy() |
| 3096 | + continue |
| 3097 | + |
| 3098 | + # Include only grantees not in config2 |
| 3099 | + cf_grantees2 = cf_config2[cf_key] |
| 3100 | + diff_grantees = [] |
| 3101 | + for grantee in grantees: |
| 3102 | + if grantee.casefold() not in cf_grantees2: |
| 3103 | + diff_grantees.append(grantee) |
| 3104 | + if diff_grantees: |
| 3105 | + diffs[key] = diff_grantees |
| 3106 | + return diffs |
| 3107 | + |
| 3108 | + return _diffs(new_config, old_config), _diffs(old_config, new_config) |
| 3109 | + |
| 3110 | + def _get_current_grants_config(self, table: exp.Table) -> GrantsConfig: |
| 3111 | + """Returns current grants for a table as a dictionary. |
| 3112 | +
|
| 3113 | + This method queries the database and returns the current grants/permissions |
| 3114 | + for the given table, parsed into a dictionary format. The it handles |
| 3115 | + case-insensitive comparison between these current grants and the desired |
| 3116 | + grants from model configuration. |
| 3117 | +
|
| 3118 | + Args: |
| 3119 | + table: The table/view to query grants for. |
| 3120 | +
|
| 3121 | + Returns: |
| 3122 | + Dictionary mapping permissions to lists of grantees. Permission names |
| 3123 | + should be returned as the database provides them (typically uppercase |
| 3124 | + for standard SQL permissions, but engine-specific roles may vary). |
| 3125 | +
|
| 3126 | + Raises: |
| 3127 | + NotImplementedError: If the engine does not support grants. |
| 3128 | + """ |
| 3129 | + if not self.SUPPORTS_GRANTS: |
| 3130 | + raise NotImplementedError(f"Engine does not support grants: {type(self)}") |
| 3131 | + raise NotImplementedError("Subclass must implement get_current_grants") |
| 3132 | + |
| 3133 | + def _apply_grants_config_expr( |
| 3134 | + self, |
| 3135 | + table: exp.Table, |
| 3136 | + grants_config: GrantsConfig, |
| 3137 | + table_type: DataObjectType = DataObjectType.TABLE, |
| 3138 | + ) -> t.List[exp.Expression]: |
| 3139 | + """Returns SQLGlot Grant expressions to apply grants to a table. |
| 3140 | +
|
| 3141 | + Args: |
| 3142 | + table: The table/view to grant permissions on. |
| 3143 | + grants_config: Dictionary mapping permissions to lists of grantees. |
| 3144 | + table_type: The type of database object (TABLE, VIEW, MATERIALIZED_VIEW). |
| 3145 | +
|
| 3146 | + Returns: |
| 3147 | + List of SQLGlot expressions for grant operations. |
| 3148 | +
|
| 3149 | + Raises: |
| 3150 | + NotImplementedError: If the engine does not support grants. |
| 3151 | + """ |
| 3152 | + if not self.SUPPORTS_GRANTS: |
| 3153 | + raise NotImplementedError(f"Engine does not support grants: {type(self)}") |
| 3154 | + raise NotImplementedError("Subclass must implement _apply_grants_config_expr") |
| 3155 | + |
| 3156 | + def _revoke_grants_config_expr( |
| 3157 | + self, |
| 3158 | + table: exp.Table, |
| 3159 | + grants_config: GrantsConfig, |
| 3160 | + table_type: DataObjectType = DataObjectType.TABLE, |
| 3161 | + ) -> t.List[exp.Expression]: |
| 3162 | + """Returns SQLGlot expressions to revoke grants from a table. |
| 3163 | +
|
| 3164 | + Args: |
| 3165 | + table: The table/view to revoke permissions from. |
| 3166 | + grants_config: Dictionary mapping permissions to lists of grantees. |
| 3167 | + table_type: The type of database object (TABLE, VIEW, MATERIALIZED_VIEW). |
| 3168 | +
|
| 3169 | + Returns: |
| 3170 | + List of SQLGlot expressions for revoke operations. |
| 3171 | +
|
| 3172 | + Raises: |
| 3173 | + NotImplementedError: If the engine does not support grants. |
| 3174 | + """ |
| 3175 | + if not self.SUPPORTS_GRANTS: |
| 3176 | + raise NotImplementedError(f"Engine does not support grants: {type(self)}") |
| 3177 | + raise NotImplementedError("Subclass must implement _revoke_grants_config_expr") |
| 3178 | + |
3032 | 3179 |
|
3033 | 3180 | class EngineAdapterWithIndexSupport(EngineAdapter): |
3034 | 3181 | SUPPORTS_INDEXES = True |
|
0 commit comments