Skip to content

infra(snowcap): adopt inherited grants and reconcile declared RBAC with Snowflake - #283

Merged
noel merged 29 commits into
mainfrom
infra/snowcap-inherited-grants
Aug 19, 2026
Merged

infra(snowcap): adopt inherited grants and reconcile declared RBAC with Snowflake#283
noel merged 29 commits into
mainfrom
infra/snowcap-inherited-grants

Conversation

@noel

@noel noel commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Brings the snowcap config in line with what is actually in Snowflake, then replaces every ALL + FUTURE grant 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_role in database_role_grants — the YAML loader ignored it, so the grant would vanish from the plan silently, with no error
  • inherited grants throughout — GRANT INHERITED is a syntax error without the account parameter this PR enables, and snowcap has to know to apply the parameter first

Merging this first does not fail loudly. It fails quietly, which is worse.

What changed

Inherited grants. 32 of them, replacing the ALL + FUTURE pairs the config used everywhere: the var.databases loop for z_tables_views__select, schema usage on RAW and GOVERNANCE, SNOWFLAKE_LEARNING_DB, and the GREAT_BAY_DEV database role example. account.yml turns on FEATURE_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 plan can compare it. Grants on all objects cannot be compared and were reapplied every run — the ~40 CREATE lines that appeared in every plan and were never real changes.

This completes what 54f6acd started and 2e0344d reverted.

RBAC reconciliation. Years of undeclared drift, worked through one plan at a time:

  • great_bay moves onto the same access model as balboa (database usage plus a role per schema, retiring a database-role pair used for one schema); great_bay_dev onto balboa_dev's
  • sushi_db and demo_db dropped from config — neither saw a data query in 90 days
  • analysts gain SNOWFLAKE_SAMPLE_DATA and SNOWFLAKE_LEARNING_DB
  • account_monitor added, held only by gomezn, carrying the Claude MCP connector
  • MCP server, Streamlit, stage and alert access declared rather than inherited from drift
  • svc_github_actions retired

A 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_type changes.

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 to ANALYST ownership so it matches every other developer schema.

default_role and default_warehouse are uppercase on purpose. Snowcap stores them as given, and OAuth resolves them case-sensitively — a lowercase account_monitor left 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 — database and database_role are not in --sync_resources:

DROP DATABASE SUSHI_DB;           -- as TRANSFORMER_DBT
DROP DATABASE DEMO_DB;            -- as SYSADMIN; 24h UNDROP window, 53 objects
DROP DATABASE ROLE GREAT_BAY.DR_CREATE_ROLE;
DROP DATABASE ROLE GREAT_BAY.DR_READER_ROLE;
DROP DATABASE ROLE GREAT_BAY.MY_DB_ROLE;

🤖 Generated with Claude Code

https://claude.ai/code/session_01DtvxdHFZzsbwDHtr13CTkH


Generated by Claude Code

claude and others added 23 commits August 15, 2026 19:13
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
noel added 3 commits August 17, 2026 18:26
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
@github-actions

Copy link
Copy Markdown
Contributor

🧪 dbt Workflow Status

  • Branch Validation: success
  • Deploy Marker Management: skipped
  • dbt Validations: success

All dbt validations passed

noel added 2 commits August 18, 2026 12:22
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.
@noel
noel merged commit cd3abcd into main Aug 19, 2026
6 checks passed
@noel
noel deleted the infra/snowcap-inherited-grants branch August 19, 2026 01:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants