infra(snowcap): adopt inherited grants and reconcile declared RBAC with Snowflake - #283
Merged
Conversation
Replace the per-database "all" + "future" grant pair on tables, dynamic tables, and views with a single inherited grant per object type. Our schemas are managed_access, where database-level future grants are fragile: Snowflake gives a schema-level future grant on the same object type precedence and silently ignores the database-level one, so objects created in that schema never receive SELECT. Inherited grants are not subject to that precedence rule, and Snowflake reports them back as a single durable record so snowcap can compare them against config instead of reapplying blindly on every run. Enable FEATURE_RBAC_INHERITED_GRANTS in account.yml. Both files are collected into the same snowcap manifest, so the account parameter is applied before the grants that depend on it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JpNXWZiUkvEovKPvCVdQre
Snowcap is authoritative for this account; permifrost is kept as reference documentation and is not run. The two specs had drifted apart, which made permifrost misleading to read. Role inventories now match exactly (71 roles, plus the 5 system roles that permifrost must declare and snowcap does not manage). Renames to snowcap's naming: z_base_analyst -> z_base__analyst de_analyst -> de__analyst de_business_analyst_pii -> de__analyst_pii z_policy_unmask_pii -> z_unmask__pii z_create_database -> z_account__create_database z_schema__raw -> z_schemas__db__raw Removed (no snowcap equivalent): z_db__raw_write, z_db__balboa_write, z_db__balboa_apps_write, z_policy_apply_masking, z_policy_apply_row_access, z_policy_create_database, z_policy_create_integration, z_policy_monitor_usage, z_policy_row_region_de, z_stage__resources_read, z_stage__resources_write, sqlmesh_analyst. Added: governance database and its roles, balboa_qa, great_bay, sushi_db, wh_finance, finance_team, z_tag__apply__pii, z_rap__apply__country__iso_3, the row-access region tier, the remaining z_schema__ roles, and z_stage__balboa_apps__resources__streamlit__write. Also aligned databases.yml, warehouses.yml (wh_transforming auto_suspend 120 -> 60), and users.yml with their snowcap counterparts. Divergences permifrost's vocabulary cannot express are flagged inline rather than left silent: warehouse MONITOR vs OPERATE, inherited grants, stage privileges, account-level privileges, managed_access, user types, statement timeouts, and max_data_extension_time_in_days. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JpNXWZiUkvEovKPvCVdQre
Sync mode reclaims anything it can't see in the config. Two groups of grants were showing up as drops. z_monitor and its 11 grants: Snowflake creates ADMIN_DB.MCPS.DATACOVES (a cortex agent server) when the account connects an MCP client, and z_monitor is the read-only role that client logs in as. None of it was declared, so plan wanted to drop the role, its grant to gomezn, and every privilege behind it -- including SNOWFLAKE.CORTEX_USER, without which the connector's SQL execution agent cannot run. Declared as atomic roles in mcp.yml composed into a z_monitor functional role, matching the pattern in alerts.yml and tasks.yml. The Snowflake-owned objects stay out of the config; only the access to them is managed. Analyst access: z_account__execute_task existed but was granted to no one while the plan revoked EXECUTE TASK and EXECUTE MANAGED TASK from analyst, which would have left nobody in the account able to execute a task. Slack had the same shape -- the atomic role only reached loader, so analysts lost notification access that email kept. Both are now on z_base__analyst, alongside roles for the Streamlit LOANS app and the EXT_JSONFILES_STAGE external stage, which analysts held directly and had no replacement. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DtvxdHFZzsbwDHtr13CTkH
z_db__admin_db and z_schema__mcps were hand-declared in mcp.yml, duplicating what object_templates/database.yml and object_templates/schema.yml already do for every other database and schema. Declare ADMIN_DB in databases.yml and ADMIN_DB.MCPS in schemas.yml instead, so the templates emit both roles and their USAGE grants. ADMIN_DB is declared with owner accountadmin to match its existing owner rather than plan a transfer, and max_data_extension_time_in_days 14, which is the value already in effect. mcp.yml keeps only what no template covers: the cortex agent server role and the account-level MONITOR role. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DtvxdHFZzsbwDHtr13CTkH
The connector authenticates as a person, so it can work off that person's analyst access instead of a dedicated role. z_monitor also broke the naming convention in README.md -- z_ marks atomic building blocks named z_<type>__<object>, and z_monitor was neither, while being assigned directly to a user. Grant the atomic roles it needs to z_base__analyst instead: z_db__admin_db, z_schema__mcps and z_mcp__datacoves reach the cortex agent server, and SNOWFLAKE.CORTEX_USER lets the agent run. Warehouse access it already had through z_wh__wh_transforming. Left off analyst deliberately: MONITOR on ACCOUNT and the OBJECT_VIEWER, GOVERNANCE_VIEWER and USAGE_VIEWER database roles. Those are account-wide metadata privileges the connector does not need to query data, and GOVERNANCE_VIEWER in particular exposes masking and row access policy definitions. z_account__monitor is dropped rather than left declared and granted to no one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DtvxdHFZzsbwDHtr13CTkH
Default roles had drifted out of band and snowcap was not managing them. Three were invalid: svc_airbyte pointed at TEST_LOADER and svc_github_actions at TRANSFORMER_DBT_PRD, neither of which exists, and svc_airflow pointed at analyst, which it is not granted. svc_fivetran and svc_superset had none at all, and gomezn pointed at z_monitor, which this branch drops. Every default is now a role the user is actually granted, and no human or service account defaults into accountadmin, securityadmin or orgadmin -- those are assumed deliberately. svc_datacoves is the one exception, since securityadmin is the only role it holds. svc_airflow defaults to transformer_dbt rather than loader: it carries the orchestrating and integration warehouses that running dbt needs, and loader only reaches wh_loading. default_role has to be set for every entry rather than left optional. The user template renders through for_each, and a missing value comes out as the literal string "None", which would then be applied as a real default role. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DtvxdHFZzsbwDHtr13CTkH
svc_github_actions last logged in 2025-04-04, 498 days ago, and its default role pointed at TRANSFORMER_DBT_PRD, a role that no longer exists. The svc_*_ci accounts created in July 2025 cover the same CI work. Removed along with its transformer_dbt grant. default_warehouse had the same drift default_role did: jesus pointed at WH_ANALYST and svc_airflow at TRANSFORMING_WAREHOUSE, neither of which exists, and sebastian, svc_datacoves, svc_datahub and svc_superset had none. Every user now names a declared warehouse. svc_airflow gets wh_orchestrating rather than wh_transforming: it is the orchestrator, transformer_dbt holds that warehouse, and the old value was already invalid so there is no working behaviour to preserve. Known imperfection: svc_datacoves defaults to wh_admin but securityadmin holds no z_wh__ role, so it cannot use it. That account runs snowcap, which needs no warehouse, and its default was previously empty -- so this is inert rather than a regression. Left explicit rather than inventing a grant to a system role. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DtvxdHFZzsbwDHtr13CTkH
ADMIN_DB.MCPS.DATACOVES is an MCP server. Snowflake reports grants on it as CORTEX_AGENT_SERVER, but GRANT ... ON CORTEX AGENT SERVER is a syntax error, so the grant has to name MCP SERVER to apply. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DtvxdHFZzsbwDHtr13CTkH
Running plan showed the two costs of putting ADMIN_DB through the object templates. roles__base.yml iterates var.databases to build z_tables_views__select, so declaring admin_db added six grants giving every analyst, de__analyst, catalog and transformer_dbt SELECT on all current and future tables and views in the admin database. And the schema template hardcodes managed_access, so ADMIN_DB.MCPS was being flipped to managed access -- a schema Snowflake created and owns, holding the agent server the connector runs through. Declare the access rather than the objects, the way databases.yml already treats covid19_epidemiological_data, snowflake and worldwide_address_data. z_db__admin_db and z_schema__mcps move to mcp.yml alongside z_mcp__datacoves. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DtvxdHFZzsbwDHtr13CTkH
BALBOA_QA is a clone of BALBOA. CREATE DATABASE ... CLONE copies grants on child objects, so every z_schema__<name> role already holds USAGE on its QA schema -- 18 grants that exist in Snowflake but appeared in no config, so sync wanted to revoke them. Analysts would have kept usage on the QA database and lost every schema inside it. Declared per-schema, not as "all schemas in database balboa_qa". Roles are scoped by layer -- finance_team reaches only L3_LOAN_ANALYTICS -- and a database-wide grant would give every such role the whole QA database, quietly turning a tiered model into a flat one. Derived from var.schemas with a where filter rather than a second list, so adding a schema to the BALBOA layer covers its QA copy automatically. The QA schemas themselves stay undeclared: the clone creates them. Requires the where filter, so plan and apply must run with -b claude/snowcap-inherited-grants-3w9m8b until that ships. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DtvxdHFZzsbwDHtr13CTkH
MONITOR on ACCOUNT is what lets the MCP connector read account metadata -- SHOW ROLES, SHOW USERS, SHOW GRANTS beyond its own role -- which is how the RBAC review in this branch was done. It reads no data and exposes no other user's queries, but it does lay out the whole grant graph: which role holds PII, who holds accountadmin, which service accounts exist and which are dormant. That is too much reach for z_base__analyst, which flows to analyst, analyst_pii, de__analyst, de__analyst_pii and catalog -- catalog being svc_datahub, dormant since May 2025. So z_account__monitor goes to a functional role granted to one person instead. account_monitor is composed from analyst rather than standing alone: the MCP connector runs as whatever role its user defaults to, so the role has to carry the data access as well as the visibility. gomezn defaults into it; everyone else is unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DtvxdHFZzsbwDHtr13CTkH
Analysts have held access to DEMO_DB.ROLES_SCHEMA since 2021, and it is the only DEMO_DB access they can actually use -- it is one of just two schemas in the account where analyst holds schema USAGE. Snowcap could not see those grants until recently, so they surfaced as drops. Keep the read half and let the rest go: USAGE on the database and schema plus SELECT on its tables and views, no DDL and no DML. Write access comes from ownership instead, with DEMO_DB moving to transformer_dbt. SELECT goes on z_tables_views__select and USAGE on the z_schema__ role, the same split every other database uses. Scoped to ROLES_SCHEMA -- analysts hold no usage on PUBLIC or SOURCE_BUSINESS and this adds none. DEMO_DB stays out of databases.yml and schemas.yml. The object templates would sweep it into the roles__base.yml for_each that gives z_tables_views__select every table in every declared database, and would put snowcap in charge of schemas it did not create. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DtvxdHFZzsbwDHtr13CTkH
…tabases SUSHI_DB and DEMO_DB are both dead. Over the last 90 days neither saw a single data query except one SELECT against DEMO_DB in May -- everything else is SHOW/DESCRIBE traffic from snowcap itself. SUSHI_DB's only content is 7 tables in a sqlmesh schema last written 2025-05; DEMO_DB's 53 objects across 4 schemas were last changed 2024-11. DEMO_DB was created by Snowflake at account provisioning (2021-01-14 14:34:21, one second before SNOWFLAKE_SAMPLE_DATA and eight seconds after ACCOUNTADMIN/PUBLIC); everything inside it was added by us afterwards. - databases.yml: remove sushi_db from var.databases. That also removes the database resource, z_db__sushi_db, its USAGE grant, and the SELECT grants the roles__base.yml for_each was producing for it. - demo_db.yml: deleted, reversing the read-only access added for analysts. - roles__functional.yml: drop z_db__sushi_db, z_db__demo_db and z_schema__roles_schema from z_base__analyst. In their place analysts get the Snowflake-provided databases. SNOWFLAKE itself was already declared; this adds: - SNOWFLAKE_SAMPLE_DATA -- a share, so IMPORTED PRIVILEGES covers the schemas and tables underneath. Snowflake grants USAGE on it to PUBLIC at provisioning time, so this is access analysts already had implicitly; the point is to hold it on a role we manage rather than on a PUBLIC grant nobody owns. - SNOWFLAKE_LEARNING_DB -- a STANDARD database, not a share, so it needs the full read stack. It is currently empty (no schemas at all), which makes the future grants the part that will actually do the work. Config loads clean: 420 resources, 7 databases, no leftover references. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DtvxdHFZzsbwDHtr13CTkH
great_bay was the only database using database roles. It had a dr_reader_role / dr_create_role pair, two functional roles wrapping them (great_bay__analyst__reader / __writer), and both granted to a single user -- a parallel access model for one schema. Neither functional role exists in Snowflake, so this apparatus was declared but never applied. Replaced with the same shape balboa uses: database USAGE via z_db__great_bay plus a role per schema, so z_schema__cove_marketing now reaches z_base__analyst the way every z_schema__* for BALBOA already does. SELECT continues to come from z_tables_views__select via the roles__base.yml for_each. Also raises max_data_extension_time_in_days from 10 to 30 to match balboa, since both are production databases. - database_roles.yml deleted - roles__functional.yml: great_bay__analyst__reader / __writer removed along with their database_role_grants; z_schema__cove_marketing added to z_base__analyst - users.yml: both roles removed from gomezn great_bay_dev gets the balboa_dev treatment. It has been owned by ANALYST since 2023 with OWNERSHIP as its only grant -- access was pure database ownership, nothing declared. It already holds per-developer schemas (BRUNO, GOMEZN, DBT_JOE), which is exactly balboa_dev's shape, where 14 of 15 schemas are ANALYST-owned developer sandboxes. - databases.yml: great_bay_dev declared, owner transformer_dbt, max_data_extension_time_in_days 5, matching balboa_dev - roles__base.yml: z_db__great_bay_dev__create_schema with CREATE SCHEMA - roles__functional.yml: z_db__great_bay_dev and its create_schema role on z_base__analyst Applying this transfers GREAT_BAY_DEV ownership from ANALYST to TRANSFORMER_DBT. Analysts trade blanket database ownership for USAGE plus CREATE SCHEMA; the schemas they already own stay theirs, and SELECT across the database now arrives through z_tables_views__select as it does for balboa_dev. Config loads clean: 418 resources, 8 databases, 81 roles, no database roles left beyond snowflake.cortex_user. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DtvxdHFZzsbwDHtr13CTkH
The previous commit retired the last database roles in this config when great_bay moved onto the account-role model, leaving no example of the feature. This restores database_roles.yml as a reference example on GREAT_BAY_DEV, the sandbox, rather than on a production database. It is deliberately inert. Analysts still reach GREAT_BAY_DEV through z_db__great_bay_dev and z_db__great_bay_dev__create_schema on z_base__analyst, exactly like BALBOA_DEV; the two account roles here carry the database roles and are granted to no user. Deleting the file would change nobody's access. The example covers the parts that are easy to get wrong: - Qualified naming (<database>.<role>), so two databases can each have a dr_reader_role without colliding. - The mandatory hop through database_role_grants. A database role cannot be granted to a user, so without an account role to carry it, it reaches nobody. - Database-wide plus future grants rather than per-schema, since GREAT_BAY_DEV's schemas are per-developer and transient. - Restating the reader's privileges on the writer instead of composing them, which is one of the ergonomic costs of database roles over account roles. The header also records why every other database here uses account roles, and the two cases where database roles are genuinely the right tool: shares, which cannot carry account roles, and database owner autonomy. One limitation noted in the file: DatabaseRoleGrant accepts to_database_role in Python, but the YAML loader reads to_role and roles: only, so nesting a database role inside another is not expressible in config. Config loads clean: 439 resources, 2 database roles, 3 database role grants. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DtvxdHFZzsbwDHtr13CTkH
Ownership of the developer schemas is load-bearing but invisible in the config, because balboa_dev does not declare its schemas either and neither should great_bay_dev. A schema is owned by the role that creates it, so an analyst creating one gets full rights inside it with no grant written down. PUBLIC is the exception in both databases: it arrives with the database and follows the database owner. Comment only, no resource change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DtvxdHFZzsbwDHtr13CTkH
…binding account_monitor looks like an ordinary functional role but is load-bearing: the Claude MCP connector's OAuth token is bound to it. CLAUDE_MCP_OAUTH sets OAUTH_ANY_ROLE_MODE = DISABLE and OAUTH_USE_SECONDARY_ROLES = NONE, so the session gets exactly one role, fixed at authorization time, with no fallback. Dropping or renaming the role invalidates the token and the connector stops working until someone re-authorizes by hand. Nothing surfaces this. There is no plan warning and no apply error -- the connector simply stops. That is how it broke today, when account_monitor replaced z_monitor and the same apply dropped z_monitor while a live token was bound to it. Comment only, no resource change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DtvxdHFZzsbwDHtr13CTkH
Snowcap renders both properties with StringProp, so the value reaches Snowflake as a quoted literal -- ALTER USER ... SET DEFAULT_ROLE = 'analyst' -- and is stored with the case given here rather than being upper-cased the way an unquoted identifier would be. Key-pair and password logins resolve that case-insensitively. Login history over the last three days shows no failures at all, including FERNANDO with 14 successful key-pair logins against a lowercase analyst, so service accounts and people were never at risk. OAuth is stricter. A lowercase account_monitor left the Claude MCP connector unable to authorize, reporting "User's configured default role 'account_monitor' does not exist or not authorized" while the role existed and was granted to the user. Setting it uppercase by hand fixed it. Uppercasing here keeps that fix from being undone. Snowcap compares these values case-sensitively -- the plan listed default_role CATALOG -> catalog as an update -- so a lowercase entry reads the corrected uppercase value in Snowflake as drift and rewrites it on the next apply, silently breaking OAuth again. Applied to all 16 users rather than only the OAuth one, so the same trap does not reappear if another user is ever pointed at an OAuth client. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DtvxdHFZzsbwDHtr13CTkH
Creating a database role grants it USAGE on the database it belongs to. Snowflake does that at creation, with an empty granted_by, and nothing can revoke it. Declaring the same usage adds a second row for a privilege the role holds either way. Dropping the two declarations changes no access and removes two grants from every apply. The header gains the point as a fourth thing worth knowing about database roles, since it is not obvious and cost real time to work out. CREATE SCHEMA on the database stays: that one is a real grant the writer needs and does not come for free. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DtvxdHFZzsbwDHtr13CTkH
The writer used to repeat every privilege the reader had, because granting a database role to another database role was not expressible in YAML. Snowcap 02d0d9b makes it expressible, so the writer now holds dr_reader_role and adds only CREATE SCHEMA. Adding a privilege to the reader gives it to the writer too. Four grants become one database role grant, and the header note that called nesting Python-only becomes a note on how to do it. No access change: the writer reaches exactly what it reached before, by inheriting rather than by repetition. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DtvxdHFZzsbwDHtr13CTkH
An inherited grant is one grant on a container that covers every object of a
type inside it, existing and future alike. It replaces the "all X in database"
plus "future X in database" pair this config used everywhere, and the intent was
already staged here: roles__base.yml carried the inherited block commented out
and account.yml carried the parameter commented out.
Three reasons it matters, beyond being fewer lines:
- Snowflake keeps an inherited grant as one durable record, so plan can
compare it against config. Grants on all objects cannot be compared, so
snowcap reapplied them every run. That is the 40 CREATE lines that appeared
in every plan today, none of which were real changes. They go away.
- A future grant at schema level silently overrides one at database level.
Inherited grants are not shadowed, so adding a schema-level grant later
cannot quietly cut off the database-level one.
- One grant record per container instead of one per object plus a future
grant.
Converted every collection grant in the config: the var.databases loop for
z_tables_views__select, schema usage for RAW and GOVERNANCE, SNOWFLAKE_LEARNING_DB,
and the GREAT_BAY_DEV database role example -- inherited grants work on database
roles exactly as on account roles.
No access change, and not by inspection: comparing the (privilege, object type,
container, grantee) coverage of every collection grant before and after gives
the same 32 targets, with nothing lost and nothing gained. Only grant_type
changes. Config drops from 432 to 401 resources.
account.yml turns the preview on. Snowcap applies account parameters before the
grants that depend on them, so one apply enables the feature and creates the
grants; the account has preview access enabled, and the parameter is currently
DISABLED, so this run is what turns it on.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtvxdHFZzsbwDHtr13CTkH
Bring the serverless alerts (raw.raw.test_alert, balboa_dev.gomezn.error_alert) and the serverless trigger task (balboa_dev.gomezn.capture_errors_task) under snowcap management, reproducing their live definitions. The two gomezn_old originals are re-homed to the gomezn schema; all three are declared SUSPENDED. Requires snowcap with serverless/triggered alert+task support (PR datacoves/ snowcap#59): run plan.sh/apply.sh with -b feat/serverless-tasks-alerts until it merges. alert/task are not in --sync_resources, so the old objects are left in place rather than dropped.
Snowflake has no ALTER for an alert's IF/THEN body, so snowcap can't reconcile it -- an update emits invalid 'ALTER ALERT ... SET then <body>'. And the body can't round-trip byte-for-byte anyway (yamllint forbids the trailing whitespace Snowflake stored), so every plan showed a spurious body change. Put condition/then under lifecycle.ignore_changes: the alerts are still declared (so they can be created) but snowcap now manages only their existence and state, leaving the body out of band.
Idempotent macro that creates the two objects snowcap can't manage for the error-capture pipeline: errors_table (shaped from stg_test_failures) and dynamic_table_stream (on stg_personal_loans). Uses ref() so they land in the same schema as the models (your dev target), replacing the manual DDL in training_and_demos/dynamic_tables/script2_advanced.sql. Run before snowcap apply: dbt run-operation create_error_capture_objects
Contributor
🧪 dbt Workflow Status
✅ All dbt validations passed |
snowcap can now express a stream on a dynamic table (PR datacoves/snowcap#59), so declare dynamic_table_stream in streams.yml instead of creating it in the dbt run-operation. The macro now only creates errors_table (which snowcap can't, no CTAS). Updates the tasks.yml note accordingly.
errors_table is now a snowcap Table (tables.yml) with columns mirroring stg_test_failures (widened so real values don't overflow), so the whole error-capture pipeline -- table, stream, task, alert -- is snowcap-managed and the create_error_capture_objects run-operation is removed. Added requires edges for apply ordering: error_alert after errors_table (its condition is validated at CREATE) and capture_errors_task after its trigger stream. stg_personal_loans stays a dbt model, referenced not managed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Brings the snowcap config in line with what is actually in Snowflake, then replaces every
ALL+FUTUREgrant pair with an inherited grant.Blocked on a snowcap release
Do not merge before datacoves/snowcap#58 ships. This config uses two things released snowcap does not have:
to_database_roleindatabase_role_grants— the YAML loader ignored it, so the grant would vanish from the plan silently, with no errorGRANT INHERITEDis a syntax error without the account parameter this PR enables, and snowcap has to know to apply the parameter firstMerging this first does not fail loudly. It fails quietly, which is worse.
What changed
Inherited grants. 32 of them, replacing the
ALL+FUTUREpairs the config used everywhere: thevar.databasesloop forz_tables_views__select, schema usage on RAW and GOVERNANCE, SNOWFLAKE_LEARNING_DB, and the GREAT_BAY_DEV database role example.account.ymlturns onFEATURE_RBAC_INHERITED_GRANTS; snowcap applies account parameters before the grants that depend on them, so one apply does both.One grant on a container covers every object of a type in it, existing and future. It also stops the plan lying: Snowflake keeps an inherited grant as a single durable record, so
plancan compare it. Grants on all objects cannot be compared and were reapplied every run — the ~40CREATElines that appeared in every plan and were never real changes.This completes what
54f6acdstarted and2e0344dreverted.RBAC reconciliation. Years of undeclared drift, worked through one plan at a time:
great_baymoves onto the same access model asbalboa(database usage plus a role per schema, retiring a database-role pair used for one schema);great_bay_devontobalboa_dev'ssushi_dbanddemo_dbdropped from config — neither saw a data query in 90 daysSNOWFLAKE_SAMPLE_DATAandSNOWFLAKE_LEARNING_DBaccount_monitoradded, held only bygomezn, carrying the Claude MCP connectorsvc_github_actionsretiredA worked database-role example on GREAT_BAY_DEV, deliberately inert: the two account roles carrying it are granted to nobody, so deleting the file changes no one's access. It documents four things about database roles that are easy to get wrong, all of which cost real time to work out here.
Access impact
None from the inherited grants migration, and checked rather than asserted: comparing the (privilege, object type, container, grantee) coverage of every collection grant before and after gives the same 32 targets, nothing lost, nothing gained. Only
grant_typechanges.The RBAC reconciliation does change access, deliberately — the plan for it was reviewed drop by drop. The one real loss, analysts' access to
GREAT_BAY_DEV.DBT_JOE, was resolved by moving that schema toANALYSTownership so it matches every other developer schema.default_roleanddefault_warehouseare uppercase on purpose. Snowcap stores them as given, and OAuth resolves them case-sensitively — a lowercaseaccount_monitorleft the MCP connector unable to authorize while the role existed and was granted.Still to do by hand after apply
Snowcap does not manage these —
databaseanddatabase_roleare not in--sync_resources:🤖 Generated with Claude Code
https://claude.ai/code/session_01DtvxdHFZzsbwDHtr13CTkH
Generated by Claude Code