Skip to content

Commit 1a08006

Browse files
committed
fix: expand grants_config parsing to support more complex expressions
1 parent 2c6c6a6 commit 1a08006

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
@@ -527,30 +527,62 @@ def custom_materialization_properties(self) -> CustomMaterializationProperties:
527527
def grants(self) -> t.Optional[GrantsConfig]:
528528
"""A dictionary of grants mapping permission names to lists of grantees."""
529529

530-
if not self.grants_:
530+
if self.grants_ is None:
531531
return None
532532

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

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

555587
@property
556588
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
@@ -11774,90 +11774,235 @@ def my_macro(evaluator):
1177411774
assert model.render_query_or_raise().sql() == 'SELECT 3 AS "c"'
1177511775

1177611776

11777-
def test_grants_validation_symbolic_model_error():
11778-
with pytest.raises(ValidationError, match=r".*grants cannot be set for EXTERNAL.*"):
11779-
create_sql_model(
11780-
"db.table",
11781-
parse_one("SELECT 1 AS id"),
11782-
kind="EXTERNAL",
11783-
grants={"select": ["user1", "user2"], "insert": ["admin_user"]},
11784-
)
11777+
@pytest.mark.parametrize(
11778+
"kind",
11779+
[
11780+
"FULL",
11781+
"VIEW",
11782+
SeedKind(path="test.csv"),
11783+
IncrementalByTimeRangeKind(time_column="ds"),
11784+
IncrementalByUniqueKeyKind(unique_key="id"),
11785+
],
11786+
)
11787+
def test_grants_valid_model_kinds(kind: t.Union[str, _ModelKind]):
11788+
model = create_sql_model(
11789+
"db.table",
11790+
parse_one("SELECT 1 AS id"),
11791+
kind=kind,
11792+
grants={"select": ["user1", "user2"], "insert": ["admin_user"]},
11793+
)
11794+
assert model.grants == {"select": ["user1", "user2"], "insert": ["admin_user"]}
1178511795

1178611796

11787-
def test_grants_validation_embedded_model_error():
11788-
with pytest.raises(ValidationError, match=r".*grants cannot be set for EMBEDDED.*"):
11797+
@pytest.mark.parametrize(
11798+
"kind",
11799+
[
11800+
"EXTERNAL",
11801+
"EMBEDDED",
11802+
],
11803+
)
11804+
def test_grants_invalid_model_kind_errors(kind: str):
11805+
with pytest.raises(ValidationError, match=rf".*grants cannot be set for {kind}.*"):
1178911806
create_sql_model(
1179011807
"db.table",
1179111808
parse_one("SELECT 1 AS id"),
11792-
kind="EMBEDDED",
11809+
kind=kind,
1179311810
grants={"select": ["user1"], "insert": ["admin_user"]},
1179411811
)
1179511812

1179611813

11797-
def test_grants_validation_valid_seed_model():
11814+
def test_grants_validation_no_grants():
11815+
model = create_sql_model("db.table", parse_one("SELECT 1 AS id"), kind="FULL")
11816+
assert model.grants is None
11817+
11818+
11819+
def test_grants_validation_empty_grantees():
1179811820
model = create_sql_model(
11799-
"db.table",
11800-
parse_one("SELECT 1 AS id"),
11801-
kind=SeedKind(path="test.csv"),
11802-
grants={"select": ["user1"], "insert": ["admin_user"]},
11821+
"db.table", parse_one("SELECT 1 AS id"), kind="FULL", grants={"select": []}
1180311822
)
11804-
assert model.grants == {"select": ["user1"], "insert": ["admin_user"]}
11823+
assert model.grants == {"select": []}
1180511824

1180611825

11807-
def test_grants_validation_valid_materialized_model():
11826+
def test_grants_single_value_conversions():
11827+
expressions = d.parse(f"""
11828+
MODEL (
11829+
name test.nested_arrays,
11830+
kind FULL,
11831+
grants (
11832+
'select' = "user1", update = user2
11833+
)
11834+
);
11835+
SELECT 1 as id
11836+
""")
11837+
model = load_sql_based_model(expressions)
11838+
assert model.grants == {"select": ["user1"], "update": ["user2"]}
11839+
1180811840
model = create_sql_model(
1180911841
"db.table",
1181011842
parse_one("SELECT 1 AS id"),
1181111843
kind="FULL",
11812-
grants={"select": ["user1", "user2"], "insert": ["admin_user"]},
11844+
grants={"select": "user1", "insert": 123},
1181311845
)
11814-
assert model.grants == {"select": ["user1", "user2"], "insert": ["admin_user"]}
11846+
assert model.grants == {"select": ["user1"], "insert": ["123"]}
1181511847

1181611848

11817-
def test_grants_validation_valid_view_model():
11818-
model = create_sql_model(
11819-
"db.table", parse_one("SELECT 1 AS id"), kind="VIEW", grants={"select": ["user1", "user2"]}
11849+
@pytest.mark.parametrize(
11850+
"grantees",
11851+
[
11852+
"('user1', ('user2', 'user3'), 'user4')",
11853+
"('user1', ['user2', 'user3'], user4)",
11854+
"['user1', ['user2', user3], 'user4']",
11855+
"[user1, ('user2', \"user3\"), 'user4']",
11856+
],
11857+
)
11858+
def test_grants_array_flattening(grantees: str):
11859+
expressions = d.parse(f"""
11860+
MODEL (
11861+
name test.nested_arrays,
11862+
kind FULL,
11863+
grants (
11864+
'select' = {grantees}
11865+
)
11866+
);
11867+
SELECT 1 as id
11868+
""")
11869+
model = load_sql_based_model(expressions)
11870+
assert model.grants == {"select": ["user1", "user2", "user3", "user4"]}
11871+
11872+
11873+
def test_grants_macro_var_resolved():
11874+
expressions = d.parse("""
11875+
MODEL (
11876+
name test.macro_grants,
11877+
kind FULL,
11878+
grants (
11879+
'select' = @VAR('readers'),
11880+
'insert' = @VAR('writers')
11881+
)
11882+
);
11883+
SELECT 1 as id
11884+
""")
11885+
model = load_sql_based_model(
11886+
expressions, variables={"readers": ["user1", "user2"], "writers": "admin"}
1182011887
)
11821-
assert model.grants == {"select": ["user1", "user2"]}
11888+
assert model.grants == {
11889+
"select": ["user1", "user2"],
11890+
"insert": ["admin"],
11891+
}
1182211892

1182311893

11824-
def test_grants_validation_valid_incremental_model():
11825-
model = create_sql_model(
11826-
"db.table",
11827-
parse_one("SELECT 1 AS id, CURRENT_TIMESTAMP AS ts"),
11828-
kind=IncrementalByTimeRangeKind(time_column="ts"),
11829-
grants={"select": ["user1"], "update": ["admin_user"]},
11894+
def test_grants_macro_var_in_array_flattening():
11895+
expressions = d.parse("""
11896+
MODEL (
11897+
name test.macro_in_array,
11898+
kind FULL,
11899+
grants (
11900+
'select' = ['user1', @VAR('admins'), 'user3']
11901+
)
11902+
);
11903+
SELECT 1 as id
11904+
""")
11905+
11906+
model = load_sql_based_model(expressions, variables={"admins": ["admin1", "admin2"]})
11907+
assert model.grants == {"select": ["user1", "admin1", "admin2", "user3"]}
11908+
11909+
model2 = load_sql_based_model(expressions, variables={"admins": "super_admin"})
11910+
assert model2.grants == {"select": ["user1", "super_admin", "user3"]}
11911+
11912+
11913+
def test_grants_dynamic_permission_names():
11914+
expressions = d.parse("""
11915+
MODEL (
11916+
name test.dynamic_keys,
11917+
kind FULL,
11918+
grants (
11919+
@VAR('read_perm') = ['user1', 'user2'],
11920+
@VAR('write_perm') = ['admin']
11921+
)
11922+
);
11923+
SELECT 1 as id
11924+
""")
11925+
model = load_sql_based_model(
11926+
expressions, variables={"read_perm": "select", "write_perm": "insert"}
1183011927
)
11831-
assert model.grants == {"select": ["user1"], "update": ["admin_user"]}
11928+
assert model.grants == {"select": ["user1", "user2"], "insert": ["admin"]}
1183211929

1183311930

11834-
def test_grants_validation_no_grants():
11835-
model = create_sql_model("db.table", parse_one("SELECT 1 AS id"), kind="FULL")
11836-
assert model.grants is None
11931+
def test_grants_unresolved_macro_errors():
11932+
expressions1 = d.parse("""
11933+
MODEL (name test.bad1, kind FULL, grants ('select' = @VAR('undefined')));
11934+
SELECT 1 as id
11935+
""")
11936+
with pytest.raises(ConfigError, match=r"Invalid grants configuration for 'select': NULL value"):
11937+
load_sql_based_model(expressions1)
1183711938

11939+
expressions2 = d.parse("""
11940+
MODEL (name test.bad2, kind FULL, grants (@VAR('undefined') = ['user']));
11941+
SELECT 1 as id
11942+
""")
11943+
with pytest.raises(ConfigError, match=r"Invalid grants configuration.*NULL value"):
11944+
load_sql_based_model(expressions2)
1183811945

11839-
def test_grants_validation_empty_grantees():
11840-
model = create_sql_model(
11946+
expressions3 = d.parse("""
11947+
MODEL (name test.bad3, kind FULL, grants ('select' = ['user', @VAR('undefined')]));
11948+
SELECT 1 as id
11949+
""")
11950+
with pytest.raises(ConfigError, match=r"Invalid grants configuration for 'select': NULL value"):
11951+
load_sql_based_model(expressions3)
11952+
11953+
11954+
def test_grants_mixed_types_conversion():
11955+
expressions = d.parse("""
11956+
MODEL (
11957+
name test.mixed_types,
11958+
kind FULL,
11959+
grants (
11960+
'select' = ['user1', 123, admin_role, 'user2']
11961+
)
11962+
);
11963+
SELECT 1 as id
11964+
""")
11965+
model = load_sql_based_model(expressions)
11966+
assert model.grants == {"select": ["user1", "123", "admin_role", "user2"]}
11967+
11968+
11969+
def test_grants_empty_values():
11970+
model1 = create_sql_model(
1184111971
"db.table", parse_one("SELECT 1 AS id"), kind="FULL", grants={"select": []}
1184211972
)
11843-
assert model.grants == {"select": []}
11973+
assert model1.grants == {"select": []}
1184411974

11975+
model2 = create_sql_model("db.table", parse_one("SELECT 1 AS id"), kind="FULL")
11976+
assert model2.grants is None
1184511977

11846-
def test_grants_table_type_view():
11847-
model = create_sql_model("test_view", parse_one("SELECT 1 as id"), kind="VIEW")
11848-
assert model.grants_table_type == DataObjectType.VIEW
1184911978

11979+
def test_grants_backward_compatibility():
1185011980
model = create_sql_model(
11851-
"test_mv", parse_one("SELECT 1 as id"), kind=ViewKind(materialized=True)
11981+
"db.table",
11982+
parse_one("SELECT 1 AS id"),
11983+
kind="FULL",
11984+
grants={
11985+
"select": ["user1", "user2"],
11986+
"insert": ["admin"],
11987+
"roles/bigquery.dataViewer": ["user:data_eng@company.com"],
11988+
},
1185211989
)
11853-
assert model.grants_table_type == DataObjectType.MATERIALIZED_VIEW
11854-
11855-
11856-
def test_grants_table_type_table():
11857-
model = create_sql_model("test_table", parse_one("SELECT 1 as id"), kind="FULL")
11858-
assert model.grants_table_type == DataObjectType.TABLE
11990+
assert model.grants == {
11991+
"select": ["user1", "user2"],
11992+
"insert": ["admin"],
11993+
"roles/bigquery.dataViewer": ["user:data_eng@company.com"],
11994+
}
1185911995

1186011996

11861-
def test_grants_table_type_managed():
11862-
model = create_sql_model("test_managed", parse_one("SELECT 1 as id"), kind="MANAGED")
11863-
assert model.grants_table_type == DataObjectType.MANAGED_TABLE
11997+
@pytest.mark.parametrize(
11998+
"kind, expected",
11999+
[
12000+
("VIEW", DataObjectType.VIEW),
12001+
("FULL", DataObjectType.TABLE),
12002+
("MANAGED", DataObjectType.MANAGED_TABLE),
12003+
(ViewKind(materialized=True), DataObjectType.MATERIALIZED_VIEW),
12004+
],
12005+
)
12006+
def test_grants_table_type(kind: t.Union[str, _ModelKind], expected: DataObjectType):
12007+
model = create_sql_model("test_table", parse_one("SELECT 1 as id"), kind=kind)
12008+
assert model.grants_table_type == expected

0 commit comments

Comments
 (0)