-
Notifications
You must be signed in to change notification settings - Fork 1.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
CT-808 more grant adapter tests #5452
Changes from all commits
1486367
e9899db
50b26d8
16b0333
03545a7
3fb963c
89eabc3
1a783f7
13f3ba6
c9b0ec8
64afa6b
03078ec
39c704b
27b1afd
6557c11
debc867
7f499ba
c763601
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,17 @@ | ||
{# ------- BOOLEAN MACROS --------- #} | ||
|
||
{# | ||
-- COPY GRANTS | ||
-- When a relational object (view or table) is replaced in this database, | ||
-- do previous grants carry over to the new object? This may depend on: | ||
-- whether we use alter-rename-swap versus CREATE OR REPLACE | ||
-- user-supplied configuration (e.g. copy_grants on Snowflake) | ||
-- By default, play it safe, assume TRUE: that grants ARE copied over. | ||
-- This means dbt will first "show" current grants and then calculate diffs. | ||
-- It may require an additional query than is strictly necessary, | ||
-- but better safe than sorry. | ||
#} | ||
|
||
{% macro copy_grants() %} | ||
{{ return(adapter.dispatch('copy_grants', 'dbt')()) }} | ||
{% endmacro %} | ||
|
@@ -6,6 +20,25 @@ | |
{{ return(True) }} | ||
{% endmacro %} | ||
|
||
|
||
{# | ||
-- SUPPORT MULTIPLE GRANTEES PER DCL STATEMENT | ||
-- Does this database support 'grant {privilege} to {grantee_1}, {grantee_2}, ...' | ||
-- Or must these be separate statements: | ||
-- `grant {privilege} to {grantee_1}`; | ||
-- `grant {privilege} to {grantee_2}`; | ||
-- By default, pick the former, because it's what we prefer when available. | ||
#} | ||
|
||
{% macro support_multiple_grantees_per_dcl_statement() %} | ||
{{ return(adapter.dispatch('support_multiple_grantees_per_dcl_statement', 'dbt')()) }} | ||
{% endmacro %} | ||
|
||
{%- macro default__support_multiple_grantees_per_dcl_statement() -%} | ||
{{ return(True) }} | ||
{%- endmacro -%} | ||
|
||
|
||
{% macro should_revoke(existing_relation, full_refresh_mode=True) %} | ||
|
||
{% if not existing_relation %} | ||
|
@@ -21,6 +54,8 @@ | |
|
||
{% endmacro %} | ||
|
||
{# ------- DCL STATEMENT TEMPLATES --------- #} | ||
|
||
{% macro get_show_grant_sql(relation) %} | ||
{{ return(adapter.dispatch("get_show_grant_sql", "dbt")(relation)) }} | ||
{% endmacro %} | ||
|
@@ -29,59 +64,104 @@ | |
show grants on {{ relation }} | ||
{% endmacro %} | ||
|
||
{% macro get_grant_sql(relation, grant_config) %} | ||
{{ return(adapter.dispatch('get_grant_sql', 'dbt')(relation, grant_config)) }} | ||
|
||
{% macro get_grant_sql(relation, privilege, grantees) %} | ||
{{ return(adapter.dispatch('get_grant_sql', 'dbt')(relation, privilege, grantees)) }} | ||
{% endmacro %} | ||
|
||
{%- macro default__get_grant_sql(relation, privilege, grantees) -%} | ||
grant {{ privilege }} on {{ relation }} to {{ grantees | join(', ') }} | ||
{%- endmacro -%} | ||
|
||
|
||
{% macro get_revoke_sql(relation, privilege, grantees) %} | ||
{{ return(adapter.dispatch('get_revoke_sql', 'dbt')(relation, privilege, grantees)) }} | ||
{% endmacro %} | ||
|
||
{%- macro default__get_revoke_sql(relation, privilege, grantees) -%} | ||
revoke {{ privilege }} on {{ relation }} from {{ grantees | join(', ') }} | ||
{%- endmacro -%} | ||
|
||
|
||
{# ------- RUNTIME APPLICATION --------- #} | ||
|
||
{% macro get_dcl_statement_list(relation, grant_config, get_dcl_macro) %} | ||
{{ return(adapter.dispatch('get_dcl_statement_list', 'dbt')(relation, grant_config, get_dcl_macro)) }} | ||
{% endmacro %} | ||
|
||
{%- macro default__get_grant_sql(relation, grant_config) -%} | ||
{%- for privilege in grant_config.keys() -%} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Doesn't have to adjust in this PR, but have we considered move all these kind of logic to python vs do them in Jinja There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was thinking about this yesterday. Both {% macro get_grant_sql() %}
grant {{ privilege }} on {{ relation }} to {{ grantee }}
{% endmacro %} I could see refactoring to call this simpler macro instead, and move the unpacking/looping/array-building logic elsewhere. For what it's worth, most of the heavier lifting is really happening in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I acted on this feedback in c763601, by moving the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @jtcohen6 solid move! I also like @ChenyuLInx's notion of putting more looping logic into Python to make the Jinja simpler. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Totally. It can be fairly tricky today to call macros from within Python models ( |
||
{%- set grantees = grant_config[privilege] -%} | ||
{%- if grantees -%} | ||
{%- for grantee in grantees -%} | ||
grant {{ privilege }} on {{ relation }} to {{ grantee }}; | ||
{%- endfor -%} | ||
{%- macro default__get_dcl_statement_list(relation, grant_config, get_dcl_macro) -%} | ||
{# | ||
-- Unpack grant_config into specific privileges and the set of users who need them granted/revoked. | ||
-- Depending on whether this database supports multiple grantees per statement, pass in the list of | ||
-- all grantees per privilege, or (if not) template one statement per privilege-grantee pair. | ||
-- `get_dcl_macro` will be either `get_grant_sql` or `get_revoke_sql` | ||
#} | ||
{%- set dcl_statements = [] -%} | ||
{%- for privilege, grantees in grant_config.items() %} | ||
{%- if support_multiple_grantees_per_dcl_statement() and grantees -%} | ||
{%- set dcl = get_dcl_macro(relation, privilege, grantees) -%} | ||
{%- do dcl_statements.append(dcl) -%} | ||
{%- else -%} | ||
{%- for grantee in grantees -%} | ||
{% set dcl = get_dcl_macro(relation, privilege, [grantee]) %} | ||
{%- do dcl_statements.append(dcl) -%} | ||
{% endfor -%} | ||
{%- endif -%} | ||
{%- endfor -%} | ||
{{ return(dcl_statements) }} | ||
{%- endmacro %} | ||
|
||
{% macro get_revoke_sql(relation, grant_config) %} | ||
{{ return(adapter.dispatch("get_revoke_sql", "dbt")(relation, grant_config)) }} | ||
|
||
{% macro call_dcl_statements(dcl_statement_list) %} | ||
{{ return(adapter.dispatch("call_dcl_statements", "dbt")(dcl_statement_list)) }} | ||
{% endmacro %} | ||
|
||
{% macro default__call_dcl_statements(dcl_statement_list) %} | ||
{# | ||
-- By default, supply all grant + revoke statements in a single semicolon-separated block, | ||
-- so that they're all processed together. | ||
|
||
-- Some databases do not support this. Those adapters will need to override this macro | ||
-- to run each statement individually. | ||
#} | ||
{% call statement('grants') %} | ||
{% for dcl_statement in dcl_statement_list %} | ||
{{ dcl_statement }}; | ||
{% endfor %} | ||
{% endcall %} | ||
{% endmacro %} | ||
|
||
{% macro default__get_revoke_sql(relation, grant_config) %} | ||
{%- for privilege in grant_config.keys() -%} | ||
{%- set grantees = grant_config[privilege] -%} | ||
{%- if grantees -%} | ||
{%- for grantee in grantees -%} | ||
revoke {{ privilege }} on {{ relation }} from {{ grantee }}; | ||
{% endfor -%} | ||
{%- endif -%} | ||
{%- endfor -%} | ||
{%- endmacro -%} | ||
|
||
{% macro apply_grants(relation, grant_config, should_revoke) %} | ||
{{ return(adapter.dispatch("apply_grants", "dbt")(relation, grant_config, should_revoke)) }} | ||
{% endmacro %} | ||
|
||
{% macro default__apply_grants(relation, grant_config, should_revoke=True) %} | ||
{#-- If grant_config is {} or None, this is a no-op --#} | ||
{% 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('On ' ~ relation ~': All grants are in place, no revocation or granting needed.')}} | ||
{% endif %} | ||
{% else %} | ||
{% set needs_revoking = {} %} | ||
{% set needs_granting = grant_config %} | ||
{% if should_revoke %} | ||
{#-- We think previous grants may have carried over --#} | ||
{#-- Show current grants and calculate diffs --#} | ||
{% 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('On ' ~ relation ~': All grants are in place, no revocation or granting needed.')}} | ||
{% endif %} | ||
{% if needs_granting or needs_revoking %} | ||
{% call statement('grants') %} | ||
{{ get_revoke_sql(relation, needs_revoking) }} | ||
{{ get_grant_sql(relation, needs_granting) }} | ||
{% endcall %} | ||
{% else %} | ||
{#-- We don't think there's any chance of previous grants having carried over. --#} | ||
{#-- Jump straight to granting what the user has configured. --#} | ||
{% set needs_revoking = {} %} | ||
{% set needs_granting = grant_config %} | ||
{% endif %} | ||
{% if needs_granting or needs_revoking %} | ||
{% set revoke_statement_list = get_dcl_statement_list(relation, needs_revoking, get_revoke_sql) %} | ||
{% set grant_statement_list = get_dcl_statement_list(relation, needs_granting, get_grant_sql) %} | ||
{% set dcl_statement_list = revoke_statement_list + grant_statement_list %} | ||
{% if dcl_statement_list %} | ||
{{ call_dcl_statements(dcl_statement_list) }} | ||
{% endif %} | ||
{% endif %} | ||
{% endif %} | ||
{% endmacro %} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
import pytest | ||
import os | ||
from dbt.tests.util import ( | ||
relation_from_name, | ||
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"] | ||
|
||
|
||
def replace_all(text, dic): | ||
for i, j in dic.items(): | ||
text = text.replace(i, j) | ||
return text | ||
|
||
|
||
class BaseGrants: | ||
def privilege_grantee_name_overrides(self): | ||
# these privilege and grantee names are valid on most databases, but not all! | ||
# looking at you, BigQuery | ||
# optionally use this to map from "select" --> "other_select_name", "insert" --> ... | ||
return { | ||
"select": "select", | ||
"insert": "insert", | ||
"fake_privilege": "fake_privilege", | ||
"invalid_user": "invalid_user", | ||
} | ||
|
||
def interpolate_name_overrides(self, yaml_text): | ||
return replace_all(yaml_text, self.privilege_grantee_name_overrides()) | ||
|
||
@pytest.fixture(scope="class", autouse=True) | ||
def get_test_users(self, project): | ||
test_users = [] | ||
for env_var in TEST_USER_ENV_VARS: | ||
user_name = os.getenv(env_var) | ||
if user_name: | ||
test_users.append(user_name) | ||
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 assert_expected_grants_match_actual(self, project, relation_name, expected_grants): | ||
actual_grants = self.get_grants_on_relation(project, relation_name) | ||
# need a case-insensitive comparison | ||
# so just a simple "assert expected == actual_grants" won't work | ||
diff_a = BaseContext.diff_of_two_dicts(actual_grants, expected_grants) | ||
diff_b = BaseContext.diff_of_two_dicts(expected_grants, actual_grants) | ||
assert diff_a == diff_b == {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
casefold, FTW 💪
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh wow thats super cool haven't heard of casefold before.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@nathaniel-may is going to take a look at refactoring this for performance / Pythonic-ness. Not going to consider it a blocker for merging this PR in the meantime.