Skip to content

Commit 85fbd39

Browse files
committed
fix: expand grants_config parsing to support more complex expressions
1 parent 2e7734d commit 85fbd39

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

526-
if not self.grants_:
526+
if self.grants_ is None:
527527
return None
528528

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

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

551583
@property
552584
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
@@ -11715,90 +11715,235 @@ def test_use_original_sql():
1171511715
assert model.post_statements_[0].sql == "CREATE TABLE post (b INT)"
1171611716

1171711717

11718-
def test_grants_validation_symbolic_model_error():
11719-
with pytest.raises(ValidationError, match=r".*grants cannot be set for EXTERNAL.*"):
11720-
create_sql_model(
11721-
"db.table",
11722-
parse_one("SELECT 1 AS id"),
11723-
kind="EXTERNAL",
11724-
grants={"select": ["user1", "user2"], "insert": ["admin_user"]},
11725-
)
11718+
@pytest.mark.parametrize(
11719+
"kind",
11720+
[
11721+
"FULL",
11722+
"VIEW",
11723+
SeedKind(path="test.csv"),
11724+
IncrementalByTimeRangeKind(time_column="ds"),
11725+
IncrementalByUniqueKeyKind(unique_key="id"),
11726+
],
11727+
)
11728+
def test_grants_valid_model_kinds(kind: t.Union[str, _ModelKind]):
11729+
model = create_sql_model(
11730+
"db.table",
11731+
parse_one("SELECT 1 AS id"),
11732+
kind=kind,
11733+
grants={"select": ["user1", "user2"], "insert": ["admin_user"]},
11734+
)
11735+
assert model.grants == {"select": ["user1", "user2"], "insert": ["admin_user"]}
1172611736

1172711737

11728-
def test_grants_validation_embedded_model_error():
11729-
with pytest.raises(ValidationError, match=r".*grants cannot be set for EMBEDDED.*"):
11738+
@pytest.mark.parametrize(
11739+
"kind",
11740+
[
11741+
"EXTERNAL",
11742+
"EMBEDDED",
11743+
],
11744+
)
11745+
def test_grants_invalid_model_kind_errors(kind: str):
11746+
with pytest.raises(ValidationError, match=rf".*grants cannot be set for {kind}.*"):
1173011747
create_sql_model(
1173111748
"db.table",
1173211749
parse_one("SELECT 1 AS id"),
11733-
kind="EMBEDDED",
11750+
kind=kind,
1173411751
grants={"select": ["user1"], "insert": ["admin_user"]},
1173511752
)
1173611753

1173711754

11738-
def test_grants_validation_valid_seed_model():
11755+
def test_grants_validation_no_grants():
11756+
model = create_sql_model("db.table", parse_one("SELECT 1 AS id"), kind="FULL")
11757+
assert model.grants is None
11758+
11759+
11760+
def test_grants_validation_empty_grantees():
1173911761
model = create_sql_model(
11740-
"db.table",
11741-
parse_one("SELECT 1 AS id"),
11742-
kind=SeedKind(path="test.csv"),
11743-
grants={"select": ["user1"], "insert": ["admin_user"]},
11762+
"db.table", parse_one("SELECT 1 AS id"), kind="FULL", grants={"select": []}
1174411763
)
11745-
assert model.grants == {"select": ["user1"], "insert": ["admin_user"]}
11764+
assert model.grants == {"select": []}
1174611765

1174711766

11748-
def test_grants_validation_valid_materialized_model():
11767+
def test_grants_single_value_conversions():
11768+
expressions = d.parse(f"""
11769+
MODEL (
11770+
name test.nested_arrays,
11771+
kind FULL,
11772+
grants (
11773+
'select' = "user1", update = user2
11774+
)
11775+
);
11776+
SELECT 1 as id
11777+
""")
11778+
model = load_sql_based_model(expressions)
11779+
assert model.grants == {"select": ["user1"], "update": ["user2"]}
11780+
1174911781
model = create_sql_model(
1175011782
"db.table",
1175111783
parse_one("SELECT 1 AS id"),
1175211784
kind="FULL",
11753-
grants={"select": ["user1", "user2"], "insert": ["admin_user"]},
11785+
grants={"select": "user1", "insert": 123},
1175411786
)
11755-
assert model.grants == {"select": ["user1", "user2"], "insert": ["admin_user"]}
11787+
assert model.grants == {"select": ["user1"], "insert": ["123"]}
1175611788

1175711789

11758-
def test_grants_validation_valid_view_model():
11759-
model = create_sql_model(
11760-
"db.table", parse_one("SELECT 1 AS id"), kind="VIEW", grants={"select": ["user1", "user2"]}
11790+
@pytest.mark.parametrize(
11791+
"grantees",
11792+
[
11793+
"('user1', ('user2', 'user3'), 'user4')",
11794+
"('user1', ['user2', 'user3'], user4)",
11795+
"['user1', ['user2', user3], 'user4']",
11796+
"[user1, ('user2', \"user3\"), 'user4']",
11797+
],
11798+
)
11799+
def test_grants_array_flattening(grantees: str):
11800+
expressions = d.parse(f"""
11801+
MODEL (
11802+
name test.nested_arrays,
11803+
kind FULL,
11804+
grants (
11805+
'select' = {grantees}
11806+
)
11807+
);
11808+
SELECT 1 as id
11809+
""")
11810+
model = load_sql_based_model(expressions)
11811+
assert model.grants == {"select": ["user1", "user2", "user3", "user4"]}
11812+
11813+
11814+
def test_grants_macro_var_resolved():
11815+
expressions = d.parse("""
11816+
MODEL (
11817+
name test.macro_grants,
11818+
kind FULL,
11819+
grants (
11820+
'select' = @VAR('readers'),
11821+
'insert' = @VAR('writers')
11822+
)
11823+
);
11824+
SELECT 1 as id
11825+
""")
11826+
model = load_sql_based_model(
11827+
expressions, variables={"readers": ["user1", "user2"], "writers": "admin"}
1176111828
)
11762-
assert model.grants == {"select": ["user1", "user2"]}
11829+
assert model.grants == {
11830+
"select": ["user1", "user2"],
11831+
"insert": ["admin"],
11832+
}
1176311833

1176411834

11765-
def test_grants_validation_valid_incremental_model():
11766-
model = create_sql_model(
11767-
"db.table",
11768-
parse_one("SELECT 1 AS id, CURRENT_TIMESTAMP AS ts"),
11769-
kind=IncrementalByTimeRangeKind(time_column="ts"),
11770-
grants={"select": ["user1"], "update": ["admin_user"]},
11835+
def test_grants_macro_var_in_array_flattening():
11836+
expressions = d.parse("""
11837+
MODEL (
11838+
name test.macro_in_array,
11839+
kind FULL,
11840+
grants (
11841+
'select' = ['user1', @VAR('admins'), 'user3']
11842+
)
11843+
);
11844+
SELECT 1 as id
11845+
""")
11846+
11847+
model = load_sql_based_model(expressions, variables={"admins": ["admin1", "admin2"]})
11848+
assert model.grants == {"select": ["user1", "admin1", "admin2", "user3"]}
11849+
11850+
model2 = load_sql_based_model(expressions, variables={"admins": "super_admin"})
11851+
assert model2.grants == {"select": ["user1", "super_admin", "user3"]}
11852+
11853+
11854+
def test_grants_dynamic_permission_names():
11855+
expressions = d.parse("""
11856+
MODEL (
11857+
name test.dynamic_keys,
11858+
kind FULL,
11859+
grants (
11860+
@VAR('read_perm') = ['user1', 'user2'],
11861+
@VAR('write_perm') = ['admin']
11862+
)
11863+
);
11864+
SELECT 1 as id
11865+
""")
11866+
model = load_sql_based_model(
11867+
expressions, variables={"read_perm": "select", "write_perm": "insert"}
1177111868
)
11772-
assert model.grants == {"select": ["user1"], "update": ["admin_user"]}
11869+
assert model.grants == {"select": ["user1", "user2"], "insert": ["admin"]}
1177311870

1177411871

11775-
def test_grants_validation_no_grants():
11776-
model = create_sql_model("db.table", parse_one("SELECT 1 AS id"), kind="FULL")
11777-
assert model.grants is None
11872+
def test_grants_unresolved_macro_errors():
11873+
expressions1 = d.parse("""
11874+
MODEL (name test.bad1, kind FULL, grants ('select' = @VAR('undefined')));
11875+
SELECT 1 as id
11876+
""")
11877+
with pytest.raises(ConfigError, match=r"Invalid grants configuration for 'select': NULL value"):
11878+
load_sql_based_model(expressions1)
1177811879

11880+
expressions2 = d.parse("""
11881+
MODEL (name test.bad2, kind FULL, grants (@VAR('undefined') = ['user']));
11882+
SELECT 1 as id
11883+
""")
11884+
with pytest.raises(ConfigError, match=r"Invalid grants configuration.*NULL value"):
11885+
load_sql_based_model(expressions2)
1177911886

11780-
def test_grants_validation_empty_grantees():
11781-
model = create_sql_model(
11887+
expressions3 = d.parse("""
11888+
MODEL (name test.bad3, kind FULL, grants ('select' = ['user', @VAR('undefined')]));
11889+
SELECT 1 as id
11890+
""")
11891+
with pytest.raises(ConfigError, match=r"Invalid grants configuration for 'select': NULL value"):
11892+
load_sql_based_model(expressions3)
11893+
11894+
11895+
def test_grants_mixed_types_conversion():
11896+
expressions = d.parse("""
11897+
MODEL (
11898+
name test.mixed_types,
11899+
kind FULL,
11900+
grants (
11901+
'select' = ['user1', 123, admin_role, 'user2']
11902+
)
11903+
);
11904+
SELECT 1 as id
11905+
""")
11906+
model = load_sql_based_model(expressions)
11907+
assert model.grants == {"select": ["user1", "123", "admin_role", "user2"]}
11908+
11909+
11910+
def test_grants_empty_values():
11911+
model1 = create_sql_model(
1178211912
"db.table", parse_one("SELECT 1 AS id"), kind="FULL", grants={"select": []}
1178311913
)
11784-
assert model.grants == {"select": []}
11914+
assert model1.grants == {"select": []}
1178511915

11916+
model2 = create_sql_model("db.table", parse_one("SELECT 1 AS id"), kind="FULL")
11917+
assert model2.grants is None
1178611918

11787-
def test_grants_table_type_view():
11788-
model = create_sql_model("test_view", parse_one("SELECT 1 as id"), kind="VIEW")
11789-
assert model.grants_table_type == DataObjectType.VIEW
1179011919

11920+
def test_grants_backward_compatibility():
1179111921
model = create_sql_model(
11792-
"test_mv", parse_one("SELECT 1 as id"), kind=ViewKind(materialized=True)
11922+
"db.table",
11923+
parse_one("SELECT 1 AS id"),
11924+
kind="FULL",
11925+
grants={
11926+
"select": ["user1", "user2"],
11927+
"insert": ["admin"],
11928+
"roles/bigquery.dataViewer": ["user:data_eng@company.com"],
11929+
},
1179311930
)
11794-
assert model.grants_table_type == DataObjectType.MATERIALIZED_VIEW
11795-
11796-
11797-
def test_grants_table_type_table():
11798-
model = create_sql_model("test_table", parse_one("SELECT 1 as id"), kind="FULL")
11799-
assert model.grants_table_type == DataObjectType.TABLE
11931+
assert model.grants == {
11932+
"select": ["user1", "user2"],
11933+
"insert": ["admin"],
11934+
"roles/bigquery.dataViewer": ["user:data_eng@company.com"],
11935+
}
1180011936

1180111937

11802-
def test_grants_table_type_managed():
11803-
model = create_sql_model("test_managed", parse_one("SELECT 1 as id"), kind="MANAGED")
11804-
assert model.grants_table_type == DataObjectType.MANAGED_TABLE
11938+
@pytest.mark.parametrize(
11939+
"kind, expected",
11940+
[
11941+
("VIEW", DataObjectType.VIEW),
11942+
("FULL", DataObjectType.TABLE),
11943+
("MANAGED", DataObjectType.MANAGED_TABLE),
11944+
(ViewKind(materialized=True), DataObjectType.MATERIALIZED_VIEW),
11945+
],
11946+
)
11947+
def test_grants_table_type(kind: t.Union[str, _ModelKind], expected: DataObjectType):
11948+
model = create_sql_model("test_table", parse_one("SELECT 1 as id"), kind=kind)
11949+
assert model.grants_table_type == expected

0 commit comments

Comments
 (0)