Write privileges for <database_or_schema>.* generate already-applied grant statements
Describe the bug
Using the "*" table wildcard for specifying write privileges results in Permifrost generating and applying all GRANT {privileges} ON {ALL|FUTURE} {resource_type}s IN {grouping_type} {grouping_name} TO ROLE {role} statements for that role on each run, even if the exact grant statements have already been executed.
Steps To Reproduce
- Specify write privileges on all tables for any role. Both of these are valid examples:
roles:
- db_database1_rw:
privileges:
databases:
write:
- database1
schemas:
write:
- database1.*
tables:
write:
- database1.schema1.*
roles:
- db_database1_rw:
privileges:
databases:
write:
- database1
schemas:
write:
- database1.*
tables:
write:
- database1.*.*
- Run Permifrost
permifrost run roles.yml --role db_database1_rw. The expected output for the second example above (database1.*.*) is:
[SUCCESS] GRANT select, insert, update, delete, truncate, references ON FUTURE tables IN database database1 TO ROLE db_database1_rw;
[SUCCESS] GRANT select, insert, update, delete, truncate, references ON ALL tables IN database database1 TO ROLE db_database1_rw;
[SUCCESS] GRANT select ON FUTURE views IN database database1 TO ROLE db_database1_rw;
[SUCCESS] GRANT select ON ALL views IN database database1 TO ROLE db_database1_rw;
[SUCCESS] GRANT select, insert, update, delete, truncate, references ON FUTURE tables IN schema database1.schema1 TO ROLE db_database1_rw;
[SUCCESS] GRANT select, insert, update, delete, truncate, references ON ALL tables IN schema database1.schema1 TO ROLE db_database1_rw;
[SUCCESS] GRANT select ON FUTURE views IN schema database1.schema1 TO ROLE db_database1_rw;
[SUCCESS] GRANT select ON ALL views IN schema database1.schema1 TO ROLE db_database1_rw;
- Re-run Permifrost.
This can be either with or without the
--dryflag. Here is the output with--dry:
[PENDING] GRANT select, insert, update, delete, truncate, references ON FUTURE tables IN database database1 TO ROLE db_database1_rw;
[PENDING] GRANT select, insert, update, delete, truncate, references ON ALL tables IN database database1 TO ROLE db_database1_rw;
[PENDING] GRANT select ON FUTURE views IN database database1 TO ROLE db_database1_rw;
[PENDING] GRANT select ON ALL views IN database database1 TO ROLE db_database1_rw;
[PENDING] GRANT select, insert, update, delete, truncate, references ON FUTURE tables IN schema database1.schema1 TO ROLE db_database1_rw;
[PENDING] GRANT select, insert, update, delete, truncate, references ON ALL tables IN schema database1.schema1 TO ROLE db_database1_rw;
[PENDING] GRANT select ON FUTURE views IN schema database1.schema1 TO ROLE db_database1_rw;
[PENDING] GRANT select ON ALL views IN schema database1.schema1 TO ROLE db_database1_rw;
Expected behavior
The Permifrost re-run would generate no grant statements to run, as no changes had been made to the spec or the existing permissions in the database. This is the behavior seen when using the * table wildcard for READ permissions.
The output of permifrost --version:
permifrost, version 0.14.0
however, do note that I am running the pre-release version of Permifrost. Partial output of pip freeze:
permifrost @ git+https://gitlab.com/gitlab-data/permifrost.git@6ca14b50b919dbe30f952e2a1170557729082150
The operating system you're using:
macOS 12.5
The output of python --version:
Python 3.9.9
Additional context
I suspect the cause of this is a cousin of the bug @moreaupascal56 found here, it just needs to be extended. In SnowflakeGrantsGenerator._generate_table_write_grants, we check for grants already given at the database level with these lines:
table_already_granted = self.is_granted_privilege(
role, write_privileges, "table", future_database_table
)
except write_privileges is defined as such earlier in the method:
read_privileges = "select"
write_partial_privileges = "insert, update, delete, truncate, references"
write_privileges = f"{read_privileges}, {write_partial_privileges}"
My understanding of the SnowflakeGrantsGenerator.is_granted_privilege method is that it is expecting a singular privilege as input, but we are passing it along "select, insert, update, delete, truncate, references". This sets table_already_granted to False, and so all of the subsequent items appended to sql_commands as part of the if schema_name == "*" and table_view_name == "*": code block include "already_granted": False and will go ahead and re-run. That also explains why this is unique to write privileges and not read, since read only includes a single privilege type (select) and wouldn't have this problem.
Even though we do correctly loop through write_privileges_array down when generating grants at the schema level that just means for a table that already has grants, this:
for privilege in write_privileges_array:
# If any of the privileges are not granted, set already_granted to False
if not self.is_granted_privilege(
role, privilege, "table", future_table
):
table_already_granted = False
break
will only have the chance to set table_already_granted = False, which is already what it's set to from the lines above.
If this all makes sense to y'all, and I'm understanding things correctly, I am also happy to open an MR for the fix!