|
34 | 34 | import pandas as pd |
35 | 35 |
|
36 | 36 | from sqlmesh.core._typing import SchemaName, SessionProperties, TableName |
37 | | - from sqlmesh.core.engine_adapter._typing import DF, Query, QueryOrDF, SnowparkSession |
| 37 | + from sqlmesh.core.engine_adapter._typing import ( |
| 38 | + DCL, |
| 39 | + DF, |
| 40 | + GrantsConfig, |
| 41 | + Query, |
| 42 | + QueryOrDF, |
| 43 | + SnowparkSession, |
| 44 | + ) |
38 | 45 | from sqlmesh.core.node import IntervalUnit |
39 | 46 |
|
40 | 47 |
|
@@ -73,6 +80,7 @@ class SnowflakeEngineAdapter(GetCurrentCatalogFromFunctionMixin, ClusteredByMixi |
73 | 80 | MANAGED_TABLE_KIND = "DYNAMIC TABLE" |
74 | 81 | SNOWPARK = "snowpark" |
75 | 82 | SUPPORTS_QUERY_EXECUTION_TRACKING = True |
| 83 | + SUPPORTS_GRANTS = True |
76 | 84 |
|
77 | 85 | @contextlib.contextmanager |
78 | 86 | def session(self, properties: SessionProperties) -> t.Iterator[None]: |
@@ -127,6 +135,118 @@ def snowpark(self) -> t.Optional[SnowparkSession]: |
127 | 135 | def catalog_support(self) -> CatalogSupport: |
128 | 136 | return CatalogSupport.FULL_SUPPORT |
129 | 137 |
|
| 138 | + @staticmethod |
| 139 | + def _grant_object_kind(table_type: DataObjectType) -> str: |
| 140 | + if table_type == DataObjectType.VIEW: |
| 141 | + return "VIEW" |
| 142 | + if table_type == DataObjectType.MATERIALIZED_VIEW: |
| 143 | + return "MATERIALIZED VIEW" |
| 144 | + if table_type == DataObjectType.MANAGED_TABLE: |
| 145 | + return "DYNAMIC TABLE" |
| 146 | + return "TABLE" |
| 147 | + |
| 148 | + def _get_current_schema(self) -> str: |
| 149 | + """Returns the current default schema for the connection.""" |
| 150 | + result = self.fetchone("SELECT CURRENT_SCHEMA()") |
| 151 | + if not result or not result[0]: |
| 152 | + raise SQLMeshError("Unable to determine current schema") |
| 153 | + return str(result[0]) |
| 154 | + |
| 155 | + def _dcl_grants_config_expr( |
| 156 | + self, |
| 157 | + dcl_cmd: t.Type[DCL], |
| 158 | + table: exp.Table, |
| 159 | + grant_config: GrantsConfig, |
| 160 | + table_type: DataObjectType = DataObjectType.TABLE, |
| 161 | + ) -> t.List[exp.Expression]: |
| 162 | + expressions: t.List[exp.Expression] = [] |
| 163 | + if not grant_config: |
| 164 | + return expressions |
| 165 | + |
| 166 | + object_kind = self._grant_object_kind(table_type) |
| 167 | + for privilege, principals in grant_config.items(): |
| 168 | + for principal in principals: |
| 169 | + args: t.Dict[str, t.Any] = { |
| 170 | + "privileges": [exp.GrantPrivilege(this=exp.Var(this=privilege))], |
| 171 | + "securable": table.copy(), |
| 172 | + "principals": [principal], |
| 173 | + } |
| 174 | + |
| 175 | + if object_kind: |
| 176 | + args["kind"] = exp.Var(this=object_kind) |
| 177 | + |
| 178 | + expressions.append(dcl_cmd(**args)) # type: ignore[arg-type] |
| 179 | + |
| 180 | + return expressions |
| 181 | + |
| 182 | + def _apply_grants_config_expr( |
| 183 | + self, |
| 184 | + table: exp.Table, |
| 185 | + grant_config: GrantsConfig, |
| 186 | + table_type: DataObjectType = DataObjectType.TABLE, |
| 187 | + ) -> t.List[exp.Expression]: |
| 188 | + return self._dcl_grants_config_expr(exp.Grant, table, grant_config, table_type) |
| 189 | + |
| 190 | + def _revoke_grants_config_expr( |
| 191 | + self, |
| 192 | + table: exp.Table, |
| 193 | + grant_config: GrantsConfig, |
| 194 | + table_type: DataObjectType = DataObjectType.TABLE, |
| 195 | + ) -> t.List[exp.Expression]: |
| 196 | + return self._dcl_grants_config_expr(exp.Revoke, table, grant_config, table_type) |
| 197 | + |
| 198 | + def _get_current_grants_config(self, table: exp.Table) -> GrantsConfig: |
| 199 | + schema_identifier = table.args.get("db") or normalize_identifiers( |
| 200 | + exp.to_identifier(self._get_current_schema(), quoted=True), dialect=self.dialect |
| 201 | + ) |
| 202 | + catalog_identifier = table.args.get("catalog") |
| 203 | + if not catalog_identifier: |
| 204 | + current_catalog = self.get_current_catalog() |
| 205 | + if not current_catalog: |
| 206 | + raise SQLMeshError("Unable to determine current catalog for fetching grants") |
| 207 | + catalog_identifier = normalize_identifiers( |
| 208 | + exp.to_identifier(current_catalog, quoted=True), dialect=self.dialect |
| 209 | + ) |
| 210 | + catalog_identifier.set("quoted", True) |
| 211 | + table_identifier = table.args.get("this") |
| 212 | + |
| 213 | + grant_expr = ( |
| 214 | + exp.select("privilege_type", "grantee") |
| 215 | + .from_( |
| 216 | + exp.table_( |
| 217 | + "TABLE_PRIVILEGES", |
| 218 | + db="INFORMATION_SCHEMA", |
| 219 | + catalog=catalog_identifier, |
| 220 | + ) |
| 221 | + ) |
| 222 | + .where( |
| 223 | + exp.and_( |
| 224 | + exp.column("table_schema").eq(exp.Literal.string(schema_identifier.this)), |
| 225 | + exp.column("table_name").eq(exp.Literal.string(table_identifier.this)), # type: ignore |
| 226 | + exp.column("grantor").eq(exp.func("CURRENT_ROLE")), |
| 227 | + exp.column("grantee").neq(exp.func("CURRENT_ROLE")), |
| 228 | + ) |
| 229 | + ) |
| 230 | + ) |
| 231 | + |
| 232 | + results = self.fetchall(grant_expr) |
| 233 | + |
| 234 | + grants_dict: GrantsConfig = {} |
| 235 | + for privilege_raw, grantee_raw in results: |
| 236 | + if privilege_raw is None or grantee_raw is None: |
| 237 | + continue |
| 238 | + |
| 239 | + privilege = str(privilege_raw) |
| 240 | + grantee = str(grantee_raw) |
| 241 | + if not privilege or not grantee: |
| 242 | + continue |
| 243 | + |
| 244 | + grantees = grants_dict.setdefault(privilege, []) |
| 245 | + if grantee not in grantees: |
| 246 | + grantees.append(grantee) |
| 247 | + |
| 248 | + return grants_dict |
| 249 | + |
130 | 250 | def _create_catalog(self, catalog_name: exp.Identifier) -> None: |
131 | 251 | props = exp.Properties( |
132 | 252 | expressions=[exp.SchemaCommentProperty(this=exp.Literal.string(c.SQLMESH_MANAGED))] |
|
0 commit comments