Skip to content

Commit 773c71f

Browse files
committed
fix: expand grants_config parsing to support more complex expressions
1 parent d8ae69d commit 773c71f

File tree

2 files changed

+247
-70
lines changed

2 files changed

+247
-70
lines changed

sqlmesh/core/model/meta.py

Lines changed: 51 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -524,30 +524,62 @@ def custom_materialization_properties(self) -> CustomMaterializationProperties:
524524
def grants(self) -> t.Optional[GrantsConfig]:
525525
"""A dictionary of grants mapping permission names to lists of grantees."""
526526

527-
if not self.grants_:
527+
if self.grants_ is None:
528528
return None
529529

530-
def parse_exp_to_str(e: exp.Expression) -> str:
531-
if isinstance(e, exp.Literal) and e.is_string:
532-
return e.this.strip()
533-
if isinstance(e, exp.Identifier):
534-
return e.name
535-
return e.sql(dialect=self.dialect).strip()
530+
if not self.grants_.expressions:
531+
return {}
532+
533+
def expr_to_string(expr: exp.Expression, context: str) -> str:
534+
if isinstance(expr, (d.MacroFunc, d.MacroVar)):
535+
raise ConfigError(
536+
f"Unresolved macro in {context}: {expr.sql(dialect=self.dialect)}"
537+
)
538+
539+
if isinstance(expr, exp.Null):
540+
raise ConfigError(f"NULL value in {context}")
541+
542+
if isinstance(expr, exp.Literal):
543+
return str(expr.this).strip()
544+
if isinstance(expr, exp.Identifier):
545+
return expr.name
546+
if isinstance(expr, exp.Column):
547+
return expr.name
548+
return expr.sql(dialect=self.dialect).strip()
549+
550+
def normalize_to_string_list(value_expr: exp.Expression) -> t.List[str]:
551+
result = []
552+
553+
def process_expression(expr: exp.Expression) -> None:
554+
if isinstance(expr, exp.Array):
555+
for elem in expr.expressions:
556+
process_expression(elem)
557+
558+
elif isinstance(expr, (exp.Tuple, exp.Paren)):
559+
expressions = (
560+
[expr.unnest()] if isinstance(expr, exp.Paren) else expr.expressions
561+
)
562+
for elem in expressions:
563+
process_expression(elem)
564+
else:
565+
result.append(expr_to_string(expr, "grant value"))
566+
567+
process_expression(value_expr)
568+
return result
536569

537570
grants_dict = {}
538571
for eq_expr in self.grants_.expressions:
539-
permission_name = parse_exp_to_str(eq_expr.this) # left hand side
540-
grantees_expr = eq_expr.expression # right hand side
541-
if isinstance(grantees_expr, exp.Array):
542-
grantee_list = []
543-
for grantee_expr in grantees_expr.expressions:
544-
grantee = parse_exp_to_str(grantee_expr)
545-
if grantee: # skip empty strings
546-
grantee_list.append(grantee)
547-
548-
grants_dict[permission_name.strip()] = grantee_list
549-
550-
return grants_dict
572+
try:
573+
permission_name = expr_to_string(eq_expr.left, "permission name")
574+
grantee_list = normalize_to_string_list(eq_expr.expression)
575+
grants_dict[permission_name] = grantee_list
576+
except ConfigError as e:
577+
permission_name = (
578+
eq_expr.left.name if hasattr(eq_expr.left, "name") else str(eq_expr.left)
579+
)
580+
raise ConfigError(f"Invalid grants configuration for '{permission_name}': {e}")
581+
582+
return grants_dict if grants_dict else None
551583

552584
@property
553585
def all_references(self) -> t.List[Reference]:

tests/core/test_model.py

Lines changed: 196 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@
6161
model,
6262
)
6363
from sqlmesh.core.model.common import parse_expression
64-
from sqlmesh.core.model.kind import ModelKindName, _model_kind_validator
64+
from sqlmesh.core.model.kind import _ModelKind, ModelKindName, _model_kind_validator
6565
from sqlmesh.core.model.seed import CsvSettings
6666
from sqlmesh.core.node import IntervalUnit, _Node
6767
from sqlmesh.core.signal import signal
@@ -11628,90 +11628,235 @@ def test_use_original_sql():
1162811628
assert model.post_statements_[0].sql == "CREATE TABLE post (b INT)"
1162911629

1163011630

11631-
def test_grants_validation_symbolic_model_error():
11632-
with pytest.raises(ValidationError, match=r".*grants cannot be set for EXTERNAL.*"):
11633-
create_sql_model(
11634-
"db.table",
11635-
parse_one("SELECT 1 AS id"),
11636-
kind="EXTERNAL",
11637-
grants={"select": ["user1", "user2"], "insert": ["admin_user"]},
11638-
)
11631+
@pytest.mark.parametrize(
11632+
"kind",
11633+
[
11634+
"FULL",
11635+
"VIEW",
11636+
SeedKind(path="test.csv"),
11637+
IncrementalByTimeRangeKind(time_column="ds"),
11638+
IncrementalByUniqueKeyKind(unique_key="id"),
11639+
],
11640+
)
11641+
def test_grants_valid_model_kinds(kind: t.Union[str, _ModelKind]):
11642+
model = create_sql_model(
11643+
"db.table",
11644+
parse_one("SELECT 1 AS id"),
11645+
kind=kind,
11646+
grants={"select": ["user1", "user2"], "insert": ["admin_user"]},
11647+
)
11648+
assert model.grants == {"select": ["user1", "user2"], "insert": ["admin_user"]}
1163911649

1164011650

11641-
def test_grants_validation_embedded_model_error():
11642-
with pytest.raises(ValidationError, match=r".*grants cannot be set for EMBEDDED.*"):
11651+
@pytest.mark.parametrize(
11652+
"kind",
11653+
[
11654+
"EXTERNAL",
11655+
"EMBEDDED",
11656+
],
11657+
)
11658+
def test_grants_invalid_model_kind_errors(kind: str):
11659+
with pytest.raises(ValidationError, match=rf".*grants cannot be set for {kind}.*"):
1164311660
create_sql_model(
1164411661
"db.table",
1164511662
parse_one("SELECT 1 AS id"),
11646-
kind="EMBEDDED",
11663+
kind=kind,
1164711664
grants={"select": ["user1"], "insert": ["admin_user"]},
1164811665
)
1164911666

1165011667

11651-
def test_grants_validation_valid_seed_model():
11668+
def test_grants_validation_no_grants():
11669+
model = create_sql_model("db.table", parse_one("SELECT 1 AS id"), kind="FULL")
11670+
assert model.grants is None
11671+
11672+
11673+
def test_grants_validation_empty_grantees():
1165211674
model = create_sql_model(
11653-
"db.table",
11654-
parse_one("SELECT 1 AS id"),
11655-
kind=SeedKind(path="test.csv"),
11656-
grants={"select": ["user1"], "insert": ["admin_user"]},
11675+
"db.table", parse_one("SELECT 1 AS id"), kind="FULL", grants={"select": []}
1165711676
)
11658-
assert model.grants == {"select": ["user1"], "insert": ["admin_user"]}
11677+
assert model.grants == {"select": []}
1165911678

1166011679

11661-
def test_grants_validation_valid_materialized_model():
11680+
def test_grants_single_value_conversions():
11681+
expressions = d.parse(f"""
11682+
MODEL (
11683+
name test.nested_arrays,
11684+
kind FULL,
11685+
grants (
11686+
'select' = "user1", update = user2
11687+
)
11688+
);
11689+
SELECT 1 as id
11690+
""")
11691+
model = load_sql_based_model(expressions)
11692+
assert model.grants == {"select": ["user1"], "update": ["user2"]}
11693+
1166211694
model = create_sql_model(
1166311695
"db.table",
1166411696
parse_one("SELECT 1 AS id"),
1166511697
kind="FULL",
11666-
grants={"select": ["user1", "user2"], "insert": ["admin_user"]},
11698+
grants={"select": "user1", "insert": 123},
1166711699
)
11668-
assert model.grants == {"select": ["user1", "user2"], "insert": ["admin_user"]}
11700+
assert model.grants == {"select": ["user1"], "insert": ["123"]}
1166911701

1167011702

11671-
def test_grants_validation_valid_view_model():
11672-
model = create_sql_model(
11673-
"db.table", parse_one("SELECT 1 AS id"), kind="VIEW", grants={"select": ["user1", "user2"]}
11703+
@pytest.mark.parametrize(
11704+
"grantees",
11705+
[
11706+
"('user1', ('user2', 'user3'), 'user4')",
11707+
"('user1', ['user2', 'user3'], user4)",
11708+
"['user1', ['user2', user3], 'user4']",
11709+
"[user1, ('user2', \"user3\"), 'user4']",
11710+
],
11711+
)
11712+
def test_grants_array_flattening(grantees: str):
11713+
expressions = d.parse(f"""
11714+
MODEL (
11715+
name test.nested_arrays,
11716+
kind FULL,
11717+
grants (
11718+
'select' = {grantees}
11719+
)
11720+
);
11721+
SELECT 1 as id
11722+
""")
11723+
model = load_sql_based_model(expressions)
11724+
assert model.grants == {"select": ["user1", "user2", "user3", "user4"]}
11725+
11726+
11727+
def test_grants_macro_var_resolved():
11728+
expressions = d.parse("""
11729+
MODEL (
11730+
name test.macro_grants,
11731+
kind FULL,
11732+
grants (
11733+
'select' = @VAR('readers'),
11734+
'insert' = @VAR('writers')
11735+
)
11736+
);
11737+
SELECT 1 as id
11738+
""")
11739+
model = load_sql_based_model(
11740+
expressions, variables={"readers": ["user1", "user2"], "writers": "admin"}
1167411741
)
11675-
assert model.grants == {"select": ["user1", "user2"]}
11742+
assert model.grants == {
11743+
"select": ["user1", "user2"],
11744+
"insert": ["admin"],
11745+
}
1167611746

1167711747

11678-
def test_grants_validation_valid_incremental_model():
11679-
model = create_sql_model(
11680-
"db.table",
11681-
parse_one("SELECT 1 AS id, CURRENT_TIMESTAMP AS ts"),
11682-
kind=IncrementalByTimeRangeKind(time_column="ts"),
11683-
grants={"select": ["user1"], "update": ["admin_user"]},
11748+
def test_grants_macro_var_in_array_flattening():
11749+
expressions = d.parse("""
11750+
MODEL (
11751+
name test.macro_in_array,
11752+
kind FULL,
11753+
grants (
11754+
'select' = ['user1', @VAR('admins'), 'user3']
11755+
)
11756+
);
11757+
SELECT 1 as id
11758+
""")
11759+
11760+
model = load_sql_based_model(expressions, variables={"admins": ["admin1", "admin2"]})
11761+
assert model.grants == {"select": ["user1", "admin1", "admin2", "user3"]}
11762+
11763+
model2 = load_sql_based_model(expressions, variables={"admins": "super_admin"})
11764+
assert model2.grants == {"select": ["user1", "super_admin", "user3"]}
11765+
11766+
11767+
def test_grants_dynamic_permission_names():
11768+
expressions = d.parse("""
11769+
MODEL (
11770+
name test.dynamic_keys,
11771+
kind FULL,
11772+
grants (
11773+
@VAR('read_perm') = ['user1', 'user2'],
11774+
@VAR('write_perm') = ['admin']
11775+
)
11776+
);
11777+
SELECT 1 as id
11778+
""")
11779+
model = load_sql_based_model(
11780+
expressions, variables={"read_perm": "select", "write_perm": "insert"}
1168411781
)
11685-
assert model.grants == {"select": ["user1"], "update": ["admin_user"]}
11782+
assert model.grants == {"select": ["user1", "user2"], "insert": ["admin"]}
1168611783

1168711784

11688-
def test_grants_validation_no_grants():
11689-
model = create_sql_model("db.table", parse_one("SELECT 1 AS id"), kind="FULL")
11690-
assert model.grants is None
11785+
def test_grants_unresolved_macro_errors():
11786+
expressions1 = d.parse("""
11787+
MODEL (name test.bad1, kind FULL, grants ('select' = @VAR('undefined')));
11788+
SELECT 1 as id
11789+
""")
11790+
with pytest.raises(ConfigError, match=r"Invalid grants configuration for 'select': NULL value"):
11791+
load_sql_based_model(expressions1)
1169111792

11793+
expressions2 = d.parse("""
11794+
MODEL (name test.bad2, kind FULL, grants (@VAR('undefined') = ['user']));
11795+
SELECT 1 as id
11796+
""")
11797+
with pytest.raises(ConfigError, match=r"Invalid grants configuration.*NULL value"):
11798+
load_sql_based_model(expressions2)
1169211799

11693-
def test_grants_validation_empty_grantees():
11694-
model = create_sql_model(
11800+
expressions3 = d.parse("""
11801+
MODEL (name test.bad3, kind FULL, grants ('select' = ['user', @VAR('undefined')]));
11802+
SELECT 1 as id
11803+
""")
11804+
with pytest.raises(ConfigError, match=r"Invalid grants configuration for 'select': NULL value"):
11805+
load_sql_based_model(expressions3)
11806+
11807+
11808+
def test_grants_mixed_types_conversion():
11809+
expressions = d.parse("""
11810+
MODEL (
11811+
name test.mixed_types,
11812+
kind FULL,
11813+
grants (
11814+
'select' = ['user1', 123, admin_role, 'user2']
11815+
)
11816+
);
11817+
SELECT 1 as id
11818+
""")
11819+
model = load_sql_based_model(expressions)
11820+
assert model.grants == {"select": ["user1", "123", "admin_role", "user2"]}
11821+
11822+
11823+
def test_grants_empty_values():
11824+
model1 = create_sql_model(
1169511825
"db.table", parse_one("SELECT 1 AS id"), kind="FULL", grants={"select": []}
1169611826
)
11697-
assert model.grants == {"select": []}
11827+
assert model1.grants == {"select": []}
1169811828

11829+
model2 = create_sql_model("db.table", parse_one("SELECT 1 AS id"), kind="FULL")
11830+
assert model2.grants is None
1169911831

11700-
def test_grants_table_type_view():
11701-
model = create_sql_model("test_view", parse_one("SELECT 1 as id"), kind="VIEW")
11702-
assert model.grants_table_type == DataObjectType.VIEW
1170311832

11833+
def test_grants_backward_compatibility():
1170411834
model = create_sql_model(
11705-
"test_mv", parse_one("SELECT 1 as id"), kind=ViewKind(materialized=True)
11835+
"db.table",
11836+
parse_one("SELECT 1 AS id"),
11837+
kind="FULL",
11838+
grants={
11839+
"select": ["user1", "user2"],
11840+
"insert": ["admin"],
11841+
"roles/bigquery.dataViewer": ["user:data_eng@company.com"],
11842+
},
1170611843
)
11707-
assert model.grants_table_type == DataObjectType.MATERIALIZED_VIEW
11708-
11709-
11710-
def test_grants_table_type_table():
11711-
model = create_sql_model("test_table", parse_one("SELECT 1 as id"), kind="FULL")
11712-
assert model.grants_table_type == DataObjectType.TABLE
11844+
assert model.grants == {
11845+
"select": ["user1", "user2"],
11846+
"insert": ["admin"],
11847+
"roles/bigquery.dataViewer": ["user:data_eng@company.com"],
11848+
}
1171311849

1171411850

11715-
def test_grants_table_type_managed():
11716-
model = create_sql_model("test_managed", parse_one("SELECT 1 as id"), kind="MANAGED")
11717-
assert model.grants_table_type == DataObjectType.MANAGED_TABLE
11851+
@pytest.mark.parametrize(
11852+
"kind, expected",
11853+
[
11854+
("VIEW", DataObjectType.VIEW),
11855+
("FULL", DataObjectType.TABLE),
11856+
("MANAGED", DataObjectType.MANAGED_TABLE),
11857+
(ViewKind(materialized=True), DataObjectType.MATERIALIZED_VIEW),
11858+
],
11859+
)
11860+
def test_grants_table_type(kind: t.Union[str, _ModelKind], expected: DataObjectType):
11861+
model = create_sql_model("test_table", parse_one("SELECT 1 as id"), kind=kind)
11862+
assert model.grants_table_type == expected

0 commit comments

Comments
 (0)