diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ad3fcd2b..b425184c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ - In multi-query statements, prepend all queries with query comments. Use the last non-`COMMIT` query to store metadata about the model result. **Note:** this restores previous (pre-v0.21) behavior for incremental models and snapshots, which will again correctly reflect the number of rows modified in `adapter_response.rows_affected` ([#140](https://github.com/dbt-labs/dbt-snowflake/issues/140), [#147](https://github.com/dbt-labs/dbt-snowflake/issues147140), [#153](https://github.com/dbt-labs/dbt-snowflake/pull/153)) - Improve column comment handling when `persist_docs` is enabled ([#161](https://github.com/dbt-labs/dbt-snowflake/pull/161)) +### Features +- Add grants to materializations ([#168](https://github.com/dbt-labs/dbt-snowflake/issues/168), [#178](https://github.com/dbt-labs/dbt-snowflake/pull/178)) + ### Contributors - [@LewisDavies](https://github.com/LewisDavies) ([#161](https://github.com/dbt-labs/dbt-snowflake/pull/161)) diff --git a/dbt/adapters/snowflake/impl.py b/dbt/adapters/snowflake/impl.py index f2af32795..56541f16b 100644 --- a/dbt/adapters/snowflake/impl.py +++ b/dbt/adapters/snowflake/impl.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from typing import Mapping, Any, Optional, List, Union +from typing import Mapping, Any, Optional, List, Union, Dict import agate @@ -9,6 +9,7 @@ LIST_SCHEMAS_MACRO_NAME, LIST_RELATIONS_MACRO_NAME, ) +from dbt.adapters.base.meta import available from dbt.adapters.snowflake import SnowflakeConnectionManager from dbt.adapters.snowflake import SnowflakeRelation from dbt.adapters.snowflake import SnowflakeColumn @@ -158,5 +159,19 @@ def quote_seed_column(self, column: str, quote_config: Optional[bool]) -> str: else: return column + @available + def standardize_grants_dict(self, grants_table: agate.Table) -> dict: + grants_dict: Dict[str, Any] = {} + + for row in grants_table: + grantee = row["grantee_name"] + privilege = row["privilege"] + if privilege != "OWNERSHIP": + if privilege in grants_dict.keys(): + grants_dict[privilege].append(grantee) + else: + grants_dict.update({privilege: [grantee]}) + return grants_dict + def timestamp_add_sql(self, add_to: str, number: int = 1, interval: str = "hour") -> str: return f"DATEADD({interval}, {number}, {add_to})" diff --git a/dbt/include/snowflake/macros/adapters.sql b/dbt/include/snowflake/macros/adapters.sql index 041b4e565..310072651 100644 --- a/dbt/include/snowflake/macros/adapters.sql +++ b/dbt/include/snowflake/macros/adapters.sql @@ -293,3 +293,12 @@ {{ snowflake_dml_explicit_transaction(truncate_dml) }} {%- endcall %} {% endmacro %} + +{% macro snowflake__copy_grants() %} + {% set copy_grants = config.get('copy_grants', False) %} + {{ return(copy_grants) }} +{% endmacro %} + +{%- macro snowflake__support_multiple_grantees_per_dcl_statement() -%} + {{ return(False) }} +{%- endmacro -%} diff --git a/dbt/include/snowflake/macros/materializations/incremental.sql b/dbt/include/snowflake/macros/materializations/incremental.sql index 5710284f3..fc3bc34fa 100644 --- a/dbt/include/snowflake/macros/materializations/incremental.sql +++ b/dbt/include/snowflake/macros/materializations/incremental.sql @@ -35,6 +35,8 @@ {% set existing_relation = load_relation(this) %} {% set tmp_relation = make_temp_relation(this) %} + {% set grant_config = config.get('grants') %} + {#-- Validate early so we don't run SQL if the strategy is invalid --#} {% set strategy = dbt_snowflake_validate_get_incremental_strategy(config) -%} {% set on_schema_change = incremental_validate_on_schema_change(config.get('on_schema_change'), default='ignore') %} @@ -74,6 +76,11 @@ {{ run_hooks(post_hooks) }} {% set target_relation = target_relation.incorporate(type='table') %} + + {% set should_revoke = + should_revoke(existing_relation.is_table, full_refresh_mode) %} + {% do apply_grants(target_relation, grant_config, should_revoke=should_revoke) %} + {% do persist_docs(target_relation, model) %} {% do unset_query_tag(original_query_tag) %} diff --git a/dbt/include/snowflake/macros/materializations/snapshot.sql b/dbt/include/snowflake/macros/materializations/snapshot.sql index c115229cd..e79516ba2 100644 --- a/dbt/include/snowflake/macros/materializations/snapshot.sql +++ b/dbt/include/snowflake/macros/materializations/snapshot.sql @@ -1,6 +1,5 @@ {% materialization snapshot, adapter='snowflake' %} {% set original_query_tag = set_query_tag() %} - {% set relations = materialization_snapshot_default() %} {% do unset_query_tag(original_query_tag) %} diff --git a/dbt/include/snowflake/macros/materializations/table.sql b/dbt/include/snowflake/macros/materializations/table.sql index 49f97069b..b1a6739c4 100644 --- a/dbt/include/snowflake/macros/materializations/table.sql +++ b/dbt/include/snowflake/macros/materializations/table.sql @@ -4,6 +4,8 @@ {%- set identifier = model['alias'] -%} + {% set grant_config = config.get('grants') %} + {%- set old_relation = adapter.get_relation(database=database, schema=schema, identifier=identifier) -%} {%- set target_relation = api.Relation.create(identifier=identifier, schema=schema, @@ -25,6 +27,9 @@ {{ run_hooks(post_hooks) }} + {% set should_revoke = should_revoke(old_relation, full_refresh_mode=True) %} + {% do apply_grants(target_relation, grant_config, should_revoke=should_revoke) %} + {% do persist_docs(target_relation, model) %} {% do unset_query_tag(original_query_tag) %} diff --git a/dbt/include/snowflake/macros/materializations/view.sql b/dbt/include/snowflake/macros/materializations/view.sql index b3858c674..90535f365 100644 --- a/dbt/include/snowflake/macros/materializations/view.sql +++ b/dbt/include/snowflake/macros/materializations/view.sql @@ -4,6 +4,7 @@ {% set to_return = create_or_replace_view() %} {% set target_relation = this.incorporate(type='view') %} + {% do persist_docs(target_relation, model, for_columns=false) %} {% do return(to_return) %} diff --git a/dev-requirements.txt b/dev-requirements.txt index 6711f16df..0b94851fe 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,5 +1,6 @@ # install latest changes in dbt-core # TODO: how to automate switching from develop to version branches? + git+https://github.com/dbt-labs/dbt-core.git#egg=dbt-core&subdirectory=core git+https://github.com/dbt-labs/dbt-core.git#egg=dbt-tests-adapter&subdirectory=tests/adapter diff --git a/test.env.example b/test.env.example index 9fdc37a21..3bc676d2e 100644 --- a/test.env.example +++ b/test.env.example @@ -29,3 +29,7 @@ SNOWFLAKE_TEST_PASSWORD=my_password SNOWFLAKE_TEST_QUOTED_DATABASE=my_quoted_database_name SNOWFLAKE_TEST_USER=my_username SNOWFLAKE_TEST_WAREHOUSE=my_warehouse_name + +DBT_TEST_USER_1=dbt_test_role_1 +DBT_TEST_USER_2=dbt_test_role_2 +DBT_TEST_USER_3=dbt_test_role_3 diff --git a/tests/functional/adapter/test_grants.py b/tests/functional/adapter/test_grants.py new file mode 100644 index 000000000..4feac6814 --- /dev/null +++ b/tests/functional/adapter/test_grants.py @@ -0,0 +1,57 @@ +import pytest +from dbt.tests.adapter.grants.test_incremental_grants import BaseIncrementalGrants +from dbt.tests.adapter.grants.test_invalid_grants import BaseInvalidGrants +from dbt.tests.adapter.grants.test_model_grants import BaseModelGrants +from dbt.tests.adapter.grants.test_seed_grants import BaseSeedGrants +from dbt.tests.adapter.grants.test_snapshot_grants import BaseSnapshotGrants + + +class BaseCopyGrantsSnowflake: + # Try every test case without copy_grants enabled (default), + # and with copy_grants enabled (this base class) + @pytest.fixture(scope="class") + def project_config_update(self): + return { + "models": { + "+copy_grants": True, + }, + "seeds": { + "+copy_grants": True, + }, + "snapshots": { + "+copy_grants": True, + } + } + + +class TestInvalidGrantsSnowflake(BaseInvalidGrants): + def grantee_does_not_exist_error(self): + return "does not exist or not authorized" + + def privilege_does_not_exist_error(self): + return "unexpected" + + +class TestModelGrantsSnowflake(BaseModelGrants): + pass + +class TestModelGrantsCopyGrantsSnowflake(BaseCopyGrantsSnowflake, BaseModelGrants): + pass + +class TestIncrementalGrantsSnowflake(BaseIncrementalGrants): + pass + +class TestIncrementalCopyGrantsSnowflake(BaseCopyGrantsSnowflake, BaseIncrementalGrants): + pass + +class TestSeedGrantsSnowflake(BaseSeedGrants): + pass + +class TestSeedCopyGrantsSnowflake(BaseCopyGrantsSnowflake, BaseSeedGrants): + pass + +class TestSnapshotGrants(BaseSnapshotGrants): + pass + +class TestSnapshotCopyGrantsSnowflake(BaseCopyGrantsSnowflake, BaseSnapshotGrants): + pass