Compare commits

...

35 Commits

Author SHA1 Message Date
Jeremy Cohen
7173ecc0f6 Test running on Postgres + Snowflake 2022-07-07 12:35:31 +02:00
Jeremy Cohen
dc691d7bc9 Validate grants in db 2022-07-07 12:05:37 +02:00
Gerda Shank
0faaa60e73 Add test switching to a different user 2022-07-07 00:02:47 -04:00
Gerda Shank
ef5eba80cc Update test_grant_configs to use parse instead of run 2022-07-06 22:14:05 -04:00
Gerda Shank
5ecd2db0c9 Changie plus remove abstractmethod from standardize_grants_dict 2022-07-06 21:50:20 -04:00
Gerda Shank
8e9a4f39bd Create initial test for grants 2022-07-06 19:10:22 -04:00
Matthew McKnight
da557d96b7 minor update trying to fix unit test 2022-07-06 16:34:07 -05:00
Matthew McKnight
ab6be852a1 change of method type for standardize_grants_dict 2022-07-06 15:59:35 -05:00
Matthew McKnight
077e4ffe50 adding ref of diff_of_two_dicts to base keys ref 2022-07-06 15:49:50 -05:00
Matthew McKnight
0843f66687 Merge branch 'main' of github.com:dbt-labs/dbt into ct-660-grant-sql 2022-07-06 15:40:40 -05:00
Matthew McKnight
c2e9aebbfe updating after pairing with dough and jeremey incorporating the new version of should revoke logic. 2022-07-06 15:26:52 -05:00
Matthew McKnight
1d263b7c75 postgres minor change to standarize_grants_dict 2022-07-06 11:59:29 -05:00
Matthew McKnight
0597398a07 postgrest must fixes from jeremy's feedback 2022-07-06 11:51:13 -05:00
Matthew McKnight
7946a3b850 models are building but testing out small issues around revoke statement never building 2022-07-05 16:20:58 -05:00
Matthew McKnight
99b14455df adding apply_grants to create_or_replace_view.sql 2022-07-05 11:03:58 -05:00
Matthew McKnight
b3e37cb3d9 Merge branch 'main' of github.com:dbt-labs/dbt into ct-660-grant-sql 2022-06-29 09:22:50 -05:00
Matthew McKnight
6f53b3db76 changes after pairing meeting 2022-06-28 16:11:40 -05:00
Matthew McKnight
fc7e24b426 changes to loop cases 2022-06-28 11:28:38 -05:00
Matthew McKnight
138f4436fc 6/27 eod update looking into diff_grants variable not getting passed into get_revoke_sql 2022-06-27 17:05:52 -05:00
Matthew McKnight
597ab0548b starting to build out postgres get_show_grant_sql getting empty query errors hopefully will clear up as we add the other postgres versions of macros and isn't a psycopg2 issue as indicated by searching 2022-06-22 17:01:01 -05:00
Matthew McKnight
6bdb4b5152 removing logs from most materializations to better track diff of grants generation logs 2022-06-22 10:34:12 -05:00
Matthew McKnight
9079c4a9c5 working on making a context to handle the diff gathering between grant_config and curreent_grants to see what needs to be revoked, I know if we assign a role, and a model becomes dependent on it we can't drop the role now still not seeing the diff appear in log 2022-06-21 17:05:13 -05:00
Matthew McKnight
95f7ae4739 Merge branch 'main' of github.com:dbt-labs/dbt into ct-660-grant-sql 2022-06-21 10:04:59 -05:00
Matthew McKnight
528dff684d minor changes 2022-06-17 16:53:02 -05:00
Matthew McKnight
6fccef6dac name change from recipents -> grantee 2022-06-17 14:43:25 -05:00
Matthew McKnight
2fff84bb55 init attempt at applying apply_grants to all materialzations 2022-06-17 13:33:45 -05:00
Matthew McKnight
4cf705f377 minor changes to how get_revoke_sql works 2022-06-17 12:47:30 -05:00
Matthew McKnight
5fb07c1a10 minor changes, and removal of logs so people can have clean grab of code 2022-06-16 16:21:18 -05:00
Matthew McKnight
7a3e7c6f74 Merge branch 'main' of github.com:dbt-labs/dbt into ct-660-grant-sql 2022-06-16 14:09:27 -05:00
Matthew McKnight
76050da482 minor spacing changes 2022-06-15 15:43:36 -05:00
Matthew McKnight
898aa8ffed post pairing push up (does have log statements to make sure we remove) 2022-06-15 14:59:44 -05:00
Matthew McKnight
7e2b7078ec minor update to should_revoke 2022-06-14 16:20:19 -05:00
Matthew McKnight
ea35fd1454 completing init default versions of all macros being called for look over and collaboration 2022-06-14 16:10:22 -05:00
Matthew McKnight
6d4b9389b1 changes to default versions of get_show_grant_sql and get_grant_sql 2022-06-14 10:49:48 -05:00
Matthew McKnight
6e88fe702a init push or ct-660 work 2022-06-13 15:19:31 -05:00
17 changed files with 333 additions and 19 deletions

View File

@@ -0,0 +1,7 @@
kind: Under the Hood
body: Add tests for SQL grants
time: 2022-07-06T21:50:01.498562-04:00
custom:
Author: gshank
Issue: "5437"
PR: "5447"

View File

@@ -159,6 +159,7 @@ class BaseAdapter(metaclass=AdapterMeta):
- convert_datetime_type
- convert_date_type
- convert_time_type
- standardize_grants_dict
Macros:
- get_catalog
@@ -538,6 +539,26 @@ class BaseAdapter(metaclass=AdapterMeta):
"`list_relations_without_caching` is not implemented for this " "adapter!"
)
###
# Methods about grants
###
@available
def standardize_grants_dict(self, grants_table: agate.Table) -> dict:
"""Translate the result of `show grants` (or equivalent) to match the
grants which a user would configure in their project.
If relevant -- filter down to grants made BY the current user role,
and filter OUT any grants TO the current user/role (e.g. OWNERSHIP).
:param grants_table: An agate table containing the query result of
the SQL returned by get_show_grant_sql
:return: A standardized dictionary matching the `grants` config
:rtype: dict
"""
raise NotImplementedException(
"`standardize_grants_dict` is not implemented for this adapter!"
)
###
# Provided methods about relations
###
@@ -1072,6 +1093,11 @@ class BaseAdapter(metaclass=AdapterMeta):
return Compiler(self.config)
# used in apply_grants -- True is a safe default
@available
def do_i_carry_over_grants_when_an_object_is_replaced(self) -> bool:
return True
# Methods used in adapter tests
def update_column_sql(
self,

View File

@@ -657,6 +657,30 @@ class BaseContext(metaclass=ContextMeta):
print(msg)
return ""
@contextmember
@staticmethod
def diff_of_two_dicts(dict_a, dict_b):
"""
Given two dictionaries:
dict_a: {'key_x': ['value_1', 'value_2'], 'key_y': ['value_3']}
dict_b: {'key_x': ['value_1'], 'key_z': ['value_4']}
Return the same dictionary representation of dict_a MINUS dict_b
"""
dict_diff = {}
dict_a = {k.lower(): v for k, v in dict_a.items()}
dict_b = {k.lower(): v for k, v in dict_b.items()}
for k in dict_a:
if k in dict_b:
a_lowered = map(lambda x: x.lower(), dict_a[k])
b_lowered = map(lambda x: x.lower(), dict_b[k])
diff = list(set(a_lowered) - set(b_lowered))
if diff:
dict_diff.update({k: diff})
else:
dict_diff.update({k: dict_a[k]})
return dict_diff
def generate_base_context(cli_vars: Dict[str, Any]) -> Dict[str, Any]:
ctx = BaseContext(cli_vars)

View File

@@ -0,0 +1,95 @@
{% macro are_grants_copied_over_when_replaced() %}
{{ return(adapter.dispatch('are_grants_copied_over_when_replaced', 'dbt')()) }}
{% endmacro %}
{% macro default__are_grants_copied_over_when_replaced() %}
{{ return(True) }}
{% endmacro %}
{% macro do_we_need_to_show_and_revoke_grants(existing_relation, full_refresh_mode=True) %}
{% if not existing_relation %}
{#-- The table doesn't already exist, so no grants to copy over --#}
{{ return(False) }}
{% elif full_refresh_mode %}
{#-- The object is being REPLACED -- whether grants are copied over depends on the value of user config --#}
{{ return(are_grants_copied_over_when_replaced()) }}
{% else %}
{#-- The table is being merged/upserted/inserted -- grants will be carried over --#}
{{ return(True) }}
{% endif %}
{% endmacro %}
{% macro get_show_grant_sql(relation) %}
{{ return(adapter.dispatch("get_show_grant_sql", "dbt")(relation)) }}
{% endmacro %}
{% macro default__get_show_grant_sql(relation) %}
show grants on {{ relation }}
{% endmacro %}
{% macro get_grant_sql(relation, grant_config) %}
{{ return(adapter.dispatch('get_grant_sql', 'dbt')(relation, grant_config)) }}
{% endmacro %}
{%- macro default__get_grant_sql(relation, grant_config) -%}
{%- for privilege in grant_config.keys() -%}
{%- set grantees = grant_config[privilege] -%}
{%- if grantees -%}
{%- for grantee in grantees -%}
grant {{ privilege }} on {{ relation }} to {{ grantee }};
{%- endfor -%}
{%- endif -%}
{%- endfor -%}
{%- endmacro %}
{% macro get_revoke_sql(relation, grant_config) %}
{{ return(adapter.dispatch("get_revoke_sql", "dbt")(relation, grant_config)) }}
{% endmacro %}
{% macro default__get_revoke_sql(relation, grant_config) %}
{%- for privilege in grant_config.keys() -%}
{%- set grantees = [] -%}
{%- set all_grantees = grant_config[privilege] -%}
{%- for grantee in all_grantees -%}
{%- if grantee != target.user -%}
{% do grantees.append(grantee) %}
{%- endif -%}
{% endfor -%}
{%- if grantees -%}
{%- for grantee in grantees -%}
revoke {{ privilege }} on {{ relation }} from {{ grantee }};
{% endfor -%}
{%- endif -%}
{%- endfor -%}
{%- endmacro -%}
{% macro apply_grants(relation, grant_config, should_revoke) %}
{% if grant_config %}
{{ return(adapter.dispatch("apply_grants", "dbt")(relation, grant_config, should_revoke)) }}
{% endif %}
{% endmacro %}
{% macro default__apply_grants(relation, grant_config, should_revoke=True) %}
{% if grant_config %}
{% if should_revoke %}
{% set current_grants_table = run_query(get_show_grant_sql(relation)) %}
{% set current_grants_dict = adapter.standardize_grants_dict(current_grants_table) %}
{% set needs_granting = diff_of_two_dicts(grant_config, current_grants_dict) %}
{% set needs_revoking = diff_of_two_dicts(current_grants_dict, grant_config) %}
{% if not (needs_granting or needs_revoking) %}
{{ log('All grants are in place, no revocation or granting needed.')}}
{% endif %}
{% else %}
{% set needs_revoking = {} %}
{% set needs_granting = grant_config %}
{% endif %}
{% if needs_granting or needs_revoking %}
{% call statement('grants') %}
{{ get_revoke_sql(relation, needs_revoking) }}
{{ get_grant_sql(relation, needs_granting) }}
{% endcall %}
{% endif %}
{% endif %}
{% endmacro %}

View File

@@ -20,6 +20,8 @@
-- BEGIN, in a separate transaction
{%- set preexisting_intermediate_relation = load_cached_relation(intermediate_relation)-%}
{%- set preexisting_backup_relation = load_cached_relation(backup_relation) -%}
-- grab current tables grants config for comparision later on
{% set grant_config = config.get('grants') %}
{{ drop_relation_if_exists(preexisting_intermediate_relation) }}
{{ drop_relation_if_exists(preexisting_backup_relation) }}
@@ -59,6 +61,9 @@
{% do to_drop.append(backup_relation) %}
{% endif %}
{% set should_revoke = do_we_need_to_show_and_revoke_grants(existing_relation, full_refresh_mode) %}
{% do apply_grants(target_relation, grant_config, should_revoke=should_revoke) %}
{% do persist_docs(target_relation, model) %}
{% if existing_relation is none or existing_relation.is_view or should_full_refresh() %}

View File

@@ -14,6 +14,8 @@
{%- set backup_relation = make_backup_relation(target_relation, backup_relation_type) -%}
-- as above, the backup_relation should not already exist
{%- set preexisting_backup_relation = load_cached_relation(backup_relation) -%}
-- grab current tables grants config for comparision later on
{% set grant_config = config.get('grants') %}
-- drop the temp relations if they exist already in the database
{{ drop_relation_if_exists(preexisting_intermediate_relation) }}
@@ -40,6 +42,9 @@
{{ run_hooks(post_hooks, inside_transaction=True) }}
{% set should_revoke = do_we_need_to_show_and_revoke_grants(existing_relation, full_refresh_mode=True) %}
{% do apply_grants(target_relation, grant_config, should_revoke=should_revoke) %}
{% do persist_docs(target_relation, model) %}
-- `COMMIT` happens here

View File

@@ -13,12 +13,12 @@
{%- set identifier = model['alias'] -%}
{%- set old_relation = adapter.get_relation(database=database, schema=schema, identifier=identifier) -%}
{%- set exists_as_view = (old_relation is not none and old_relation.is_view) -%}
{%- set target_relation = api.Relation.create(
identifier=identifier, schema=schema, database=database,
type='view') -%}
{% set grant_config = config.get('grants') %}
{{ run_hooks(pre_hooks) }}
@@ -34,6 +34,7 @@
{{ get_create_view_as_sql(target_relation, sql) }}
{%- endcall %}
{% do apply_grants(target_relation, grant_config, should_revoke=True) %}
{{ run_hooks(post_hooks) }}
{{ return({'relations': [target_relation]}) }}

View File

@@ -25,6 +25,8 @@
{%- set backup_relation = make_backup_relation(target_relation, backup_relation_type) -%}
-- as above, the backup_relation should not already exist
{%- set preexisting_backup_relation = load_cached_relation(backup_relation) -%}
-- grab current tables grants config for comparision later on
{% set grant_config = config.get('grants') %}
{{ run_hooks(pre_hooks, inside_transaction=False) }}
@@ -47,6 +49,9 @@
{% endif %}
{{ adapter.rename_relation(intermediate_relation, target_relation) }}
{% set should_revoke = do_we_need_to_show_and_revoke_grants(existing_relation, full_refresh_mode=True) %}
{% do apply_grants(target_relation, grant_config, should_revoke=should_revoke) %}
{% do persist_docs(target_relation, model) %}
{{ run_hooks(post_hooks, inside_transaction=True) }}

View File

@@ -8,7 +8,10 @@
{%- set exists_as_table = (old_relation is not none and old_relation.is_table) -%}
{%- set exists_as_view = (old_relation is not none and old_relation.is_view) -%}
{%- set grant_config = config.get('grants') -%}
{%- set agate_table = load_agate_table() -%}
-- grab current tables grants config for comparision later on
{%- do store_result('agate_table', response='OK', agate_table=agate_table) -%}
{{ run_hooks(pre_hooks, inside_transaction=False) }}
@@ -35,6 +38,10 @@
{% endcall %}
{% set target_relation = this.incorporate(type='table') %}
{% set should_revoke = do_we_need_to_show_and_revoke_grants(old_relation, full_refresh_mode) %}
{% do apply_grants(target_relation, grant_config, should_revoke=should_revoke) %}
{% do persist_docs(target_relation, model) %}
{% if full_refresh_mode or not exists_as_table %}

View File

@@ -5,6 +5,8 @@
{%- set strategy_name = config.get('strategy') -%}
{%- set unique_key = config.get('unique_key') %}
-- grab current tables grants config for comparision later on
{%- set grant_config = config.get('grants') -%}
{% set target_relation_exists, target_relation = get_or_create_relation(
database=model.database,
@@ -73,6 +75,9 @@
{{ final_sql }}
{% endcall %}
{% set should_revoke = do_we_need_to_show_and_revoke_grants(target_relation_exists, full_refresh_mode=False) %}
{% do apply_grants(target_relation, grant_config, should_revoke=should_revoke) %}
{% do persist_docs(target_relation, model) %}
{% if not target_relation_exists %}

View File

@@ -634,7 +634,9 @@ class ManifestLoader:
if not flags.PARTIAL_PARSE:
fire_event(PartialParsingNotEnabled())
return None
path = os.path.join(self.root_project.target_path, PARTIAL_PARSE_FILE_NAME)
path = os.path.join(
self.root_project.project_root, self.root_project.target_path, PARTIAL_PARSE_FILE_NAME
)
reparse_reason = None

View File

@@ -10,6 +10,7 @@ from dbt.adapters.postgres import PostgresRelation
from dbt.dataclass_schema import dbtClassMixin, ValidationError
import dbt.exceptions
import dbt.utils
import agate
# note that this isn't an adapter macro, so just a single underscore
@@ -85,6 +86,18 @@ class PostgresAdapter(SQLAdapter):
def parse_index(self, raw_index: Any) -> Optional[PostgresIndexConfig]:
return PostgresIndexConfig.parse(raw_index)
@available
def standardize_grants_dict(self, grants_table: agate.Table) -> dict:
grants_dict = {}
for row in grants_table:
grantee = row["grantee"]
privilege = row["privilege_type"]
if privilege in grants_dict.keys():
grants_dict[privilege].append(grantee)
else:
grants_dict.update({privilege: [grantee]})
return grants_dict
def _link_cached_database_relations(self, schemas: Set[str]):
"""
:param schemas: The set of schemas that should have links added.

View File

@@ -202,3 +202,16 @@
comment on column {{ relation }}.{{ adapter.quote(column_name) if column_dict[column_name]['quote'] else column_name }} is {{ escaped_comment }};
{% endfor %}
{% endmacro %}
{%- macro postgres__get_show_grant_sql(relation) -%}
select grantee, privilege_type
from information_schema.role_table_grants
where grantor = current_role
and grantee != current_role
and table_schema = '{{ relation.schema }}'
and table_name = '{{ relation.identifier }}'
{%- endmacro -%}
{% macro postgres__are_grants_copied_over_when_replaced() %}
{{ return(False) }}
{% endmacro %}

View File

@@ -46,6 +46,9 @@ psql -c "GRANT CREATE, CONNECT ON DATABASE dbt TO root WITH GRANT OPTION;"
psql -c "CREATE ROLE noaccess WITH PASSWORD 'password' NOSUPERUSER;"
psql -c "ALTER ROLE noaccess WITH LOGIN;"
psql -c "GRANT CONNECT ON DATABASE dbt TO noaccess;"
psql -c "CREATE ROLE dbt_test_user_1;"
psql -c "CREATE ROLE dbt_test_user_2;"
psql -c "CREATE ROLE dbt_test_user_3;"
psql -c 'CREATE DATABASE "dbtMixedCase";'
psql -c 'GRANT CREATE, CONNECT ON DATABASE "dbtMixedCase" TO root WITH GRANT OPTION;'

View File

@@ -199,6 +199,7 @@ REQUIRED_BASE_KEYS = frozenset(
"modules",
"flags",
"print",
"diff_of_two_dicts"
}
)

View File

@@ -0,0 +1,108 @@
import pytest
import os
from dbt.tests.util import (
run_dbt,
get_manifest,
read_file,
relation_from_name,
rm_file,
write_file,
get_connection,
)
from dbt.context.base import BaseContext # diff_of_two_dicts only
TEST_USER_ENV_VARS = ["DBT_TEST_USER_1", "DBT_TEST_USER_2", "DBT_TEST_USER_3"]
my_model_sql = """
select 1 as fun
"""
model_schema_yml = """
version: 2
models:
- name: my_model
config:
grants:
select: ["{{ env_var('DBT_TEST_USER_1') }}"]
"""
user2_model_schema_yml = """
version: 2
models:
- name: my_model
config:
grants:
select: ["{{ env_var('DBT_TEST_USER_2') }}"]
"""
class TestModelGrants:
@pytest.fixture(scope="class")
def models(self):
return {"my_model.sql": my_model_sql, "schema.yml": model_schema_yml}
@pytest.fixture(scope="class", autouse=True)
def get_test_users(self, project):
test_users = []
missing = []
for env_var in TEST_USER_ENV_VARS:
user_name = os.getenv(env_var)
if not user_name:
missing.append(env_var)
else:
test_users.append(user_name)
if missing:
pytest.skip(f"Test requires env vars with test users. Missing {', '.join(missing)}.")
return test_users
def get_grants_on_relation(self, project, relation_name):
relation = relation_from_name(project.adapter, relation_name)
adapter = project.adapter
with get_connection(adapter):
kwargs = {"relation": relation}
show_grant_sql = adapter.execute_macro("get_show_grant_sql", kwargs=kwargs)
_, grant_table = adapter.execute(show_grant_sql, fetch=True)
actual_grants = adapter.standardize_grants_dict(grant_table)
return actual_grants
def test_basic(self, project, get_test_users, logs_dir):
# Tests a project with a single model, view materialization
results = run_dbt(["run"])
assert len(results) == 1
manifest = get_manifest(project.project_root)
model_id = "model.test.my_model"
model = manifest.nodes[model_id]
expected = {"select": [get_test_users[0]]}
assert model.config.grants == expected
assert model.config.materialized == "view"
# validate grant statements in logs
log_contents = read_file(logs_dir, "dbt.log")
my_model_relation = relation_from_name(project.adapter, "my_model")
grant_log_line = f"grant select on {my_model_relation} to {get_test_users[0]};"
assert grant_log_line in log_contents
# validate actual grants in database
actual_grants = self.get_grants_on_relation(project, "my_model")
# actual_grants: {'SELECT': ['dbt_test_user_1']}
# need a case-insensitive comparison
# so just a simple "assert expected == actual_grants" won't work
diff = BaseContext.diff_of_two_dicts(expected, actual_grants)
assert diff == {}
# Switch to a different user, still view materialization
rm_file(logs_dir, "dbt.log")
write_file(user2_model_schema_yml, project.project_root, "models", "schema.yml")
results = run_dbt(["run"])
assert len(results) == 1
log_contents = read_file(logs_dir, "dbt.log")
print(log_contents)
grant_log_line = f"grant select on {my_model_relation} to {get_test_users[1]};"
assert grant_log_line in log_contents
# Note: We are not revoking grants here, so there is no revoke in the log
expected = {"select": [get_test_users[1]]}
actual_grants = self.get_grants_on_relation(project, "my_model")
diff = BaseContext.diff_of_two_dicts(expected, actual_grants)
assert diff == {}

View File

@@ -57,11 +57,10 @@ class TestGrantConfigs:
return dbt_project_yml
def test_model_grant_config(self, project, logs_dir):
# This test uses "my_select" instead of "select", so that when
# actual granting of permissions happens, it won't break this
# test.
results = run_dbt(["run"])
assert len(results) == 1
# This test uses "my_select" instead of "select", so we need
# use "parse" instead of "run" because we will get compilation
# errors for the grants.
run_dbt(["parse"])
manifest = get_manifest(project.project_root)
model_id = "model.test.my_model"
@@ -77,7 +76,7 @@ class TestGrantConfigs:
# add model grant with clobber
write_file(my_model_clobber_sql, project.project_root, "models", "my_model.sql")
results = run_dbt(["run"])
run_dbt(["parse"])
manifest = get_manifest(project.project_root)
model_config = manifest.nodes[model_id].config
@@ -86,7 +85,7 @@ class TestGrantConfigs:
# change model to extend grants
write_file(my_model_extend_sql, project.project_root, "models", "my_model.sql")
results = run_dbt(["run"])
run_dbt(["parse"])
manifest = get_manifest(project.project_root)
model_config = manifest.nodes[model_id].config
@@ -95,8 +94,7 @@ class TestGrantConfigs:
# add schema file with extend
write_file(append_schema_yml, project.project_root, "models", "schema.yml")
results = run_dbt(["run"])
assert len(results) == 1
run_dbt(["parse"])
manifest = get_manifest(project.project_root)
model_config = manifest.nodes[model_id].config
@@ -106,8 +104,7 @@ class TestGrantConfigs:
# change model file to have string instead of list
write_file(my_model_extend_string_sql, project.project_root, "models", "my_model.sql")
results = run_dbt(["run"])
assert len(results) == 1
run_dbt(["parse"])
manifest = get_manifest(project.project_root)
model_config = manifest.nodes[model_id].config
@@ -117,8 +114,7 @@ class TestGrantConfigs:
# change model file to have string instead of list
write_file(my_model_extend_twice_sql, project.project_root, "models", "my_model.sql")
results = run_dbt(["run"])
assert len(results) == 1
run_dbt(["parse"])
manifest = get_manifest(project.project_root)
model_config = manifest.nodes[model_id].config
@@ -135,8 +131,7 @@ class TestGrantConfigs:
"log-path": logs_dir,
}
write_config_file(config, project.project_root, "dbt_project.yml")
results = run_dbt(["run"])
assert len(results) == 1
run_dbt(["parse"])
manifest = get_manifest(project.project_root)
model_config = manifest.nodes[model_id].config
@@ -146,8 +141,7 @@ class TestGrantConfigs:
# Remove my_model config, leaving only schema file
write_file(my_model_base_sql, project.project_root, "models", "my_model.sql")
results = run_dbt(["run"])
assert len(results) == 1
run_dbt(["parse"])
manifest = get_manifest(project.project_root)
model_config = manifest.nodes[model_id].config