From 8b4ade39f7643c91d9f03a6cef232cf438df075b Mon Sep 17 00:00:00 2001 From: Brandon Jackson Date: Sun, 17 May 2026 13:41:07 -0500 Subject: [PATCH 1/3] fix(database_role_grant): return filtered row + add USE AI FUNCTIONS priv MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two related fixes that together unlock modeling Snowflake Cortex permissions in snowcap. 1. fetch_database_role_grant returned show_result[0] (unfiltered first row of SHOW GRANTS) instead of role_grants[0] (the row matching the URN's requested grantee). For any database role granted to multiple account/database roles, every per-grantee fetch returned the first grantee's data — causing the diff engine to emit a spurious UpdateResource (e.g. "to_role: ACCOUNTADMIN -> DBT_DEVELOPER") whenever a second grantee was declared in YAML. There is no update_database_role_grant lifecycle hook so the apply would also fail with invalid SQL. Adds an integration regression test (test_fetch_grant_of_database_role_multiple_grantees) that grants one database role to two account roles and fetches each FQN separately; previously both fetches returned the same first-grantee row. 2. Adds USE_AI_FUNCTIONS = "USE AI FUNCTIONS" to AccountPriv. This account- level privilege gates Snowflake Cortex AI SQL functions (AI_COMPLETE, AI_FILTER, SUMMARIZE, COMPLETE, embed/search REST APIs, etc.). Without it in the enum, `priv: USE AI FUNCTIONS / on: account` in YAML raises a parse error. Together these enable native modeling of Cortex Code, Cortex AI SQL, and Cortex Agents access in YAML via `database_role_grants` (CORTEX_USER, CORTEX_AGENT_USER, COPILOT_USER, AI_FUNCTIONS_USER) and `grants` (USE AI FUNCTIONS on account). Co-Authored-By: Claude Opus 4.7 (1M context) --- snowcap/data_provider.py | 2 +- snowcap/privs.py | 1 + .../data_provider/test_fetch_resource.py | 42 +++++++++++++++++++ tests/test_privs.py | 4 ++ 4 files changed, 48 insertions(+), 1 deletion(-) diff --git a/snowcap/data_provider.py b/snowcap/data_provider.py index 3435269..72bbd54 100644 --- a/snowcap/data_provider.py +++ b/snowcap/data_provider.py @@ -1542,7 +1542,7 @@ def fetch_database_role_grant(session: SnowflakeConnection, fqn: FQN): if len(role_grants) > 1: raise Exception(f"Found multiple database role grants matching {fqn}") - data = show_result[0] + data = role_grants[0] to_role = None to_database_role = None diff --git a/snowcap/privs.py b/snowcap/privs.py index 7bf8b5b..e6909ec 100644 --- a/snowcap/privs.py +++ b/snowcap/privs.py @@ -84,6 +84,7 @@ class AccountPriv(Priv): OVERRIDE_SHARE_RESTRICTIONS = "OVERRIDE SHARE RESTRICTIONS" PURCHASE_DATA_EXCHANGE_LISTING = "PURCHASE DATA EXCHANGE LISTING" RESOLVE_ALL = "RESOLVE ALL" + USE_AI_FUNCTIONS = "USE AI FUNCTIONS" class AlertPriv(Priv): diff --git a/tests/integration/data_provider/test_fetch_resource.py b/tests/integration/data_provider/test_fetch_resource.py index a1ef2ea..7199d2a 100644 --- a/tests/integration/data_provider/test_fetch_resource.py +++ b/tests/integration/data_provider/test_fetch_resource.py @@ -663,3 +663,45 @@ def test_fetch_grant_of_database_role(cursor, suffix, marked_for_cleanup): assert result == data +def test_fetch_grant_of_database_role_multiple_grantees(cursor, suffix, marked_for_cleanup): + """Regression: a database role granted to multiple roles must fetch each grantee's + row independently. Prior to the fix in fetch_database_role_grant, every fetch + returned show_result[0] (the first SHOW GRANTS row) regardless of the URN's + requested grantee, so adding a second grantee in YAML produced a spurious + UPDATE diff (to_role: -> ) instead of CREATE. + """ + db_role = res.DatabaseRole( + name=f"TEST_FETCH_DRG_MULTI_{suffix}", + database="STATIC_DATABASE", + owner=TEST_ROLE, + ) + create(cursor, db_role) + marked_for_cleanup.append(db_role) + + role_a = res.Role(name=f"TEST_FETCH_DRG_MULTI_A_{suffix}", owner=TEST_ROLE) + role_b = res.Role(name=f"TEST_FETCH_DRG_MULTI_B_{suffix}", owner=TEST_ROLE) + create(cursor, role_a) + create(cursor, role_b) + marked_for_cleanup.append(role_a) + marked_for_cleanup.append(role_b) + + grant_a = res.DatabaseRoleGrant(database_role=db_role, to_role=role_a) + grant_b = res.DatabaseRoleGrant(database_role=db_role, to_role=role_b) + create(cursor, grant_a) + create(cursor, grant_b) + + result_a = safe_fetch(cursor, grant_a.urn) + result_b = safe_fetch(cursor, grant_b.urn) + + assert result_a is not None + assert result_b is not None + # Each fetch must return its own grantee, not the first row from SHOW GRANTS. + assert result_a != result_b + assert clean_resource_data(res.DatabaseRoleGrant.spec, result_a) == clean_resource_data( + res.DatabaseRoleGrant.spec, grant_a.to_dict() + ) + assert clean_resource_data(res.DatabaseRoleGrant.spec, result_b) == clean_resource_data( + res.DatabaseRoleGrant.spec, grant_b.to_dict() + ) + + diff --git a/tests/test_privs.py b/tests/test_privs.py index e12da12..475fe02 100644 --- a/tests/test_privs.py +++ b/tests/test_privs.py @@ -159,6 +159,10 @@ def test_apply_masking_policy_privilege(self): """AccountPriv has APPLY MASKING POLICY privilege.""" assert AccountPriv.APPLY_MASKING_POLICY.value == "APPLY MASKING POLICY" + def test_use_ai_functions_privilege(self): + """AccountPriv has USE AI FUNCTIONS privilege (required for Cortex AI SQL functions).""" + assert AccountPriv.USE_AI_FUNCTIONS.value == "USE AI FUNCTIONS" + def test_account_priv_count(self): """AccountPriv has expected number of privileges.""" # Should have at least 40 account-level privileges From 438d8e122f34801c5e556dfea39ab3a16638e48f Mon Sep 17 00:00:00 2001 From: Brandon Jackson Date: Sun, 17 May 2026 14:35:32 -0500 Subject: [PATCH 2/3] feat(cortex_search): support CORTEX SEARCH SERVICE grants MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds CORTEX SEARCH SERVICE as a schema-scoped grantable resource type so users can write: grants: - priv: USAGE on: cortex search service .. to: - priv: MONITOR on: cortex search service .. to: Snowflake's Cortex Search Service supports four privileges (USAGE, MONITOR, OWNERSHIP, ALL); USAGE is required to run SNOWFLAKE.CORTEX.SEARCH_PREVIEW and MONITOR is required for the new get_ai_observability_events() log access. The schema-scope priv CREATE CORTEX SEARCH SERVICE was already in SchemaPriv. Changes: - ResourceType.CORTEX_SEARCH_SERVICE added to the enum (alphabetical). - CortexSearchServicePriv(Priv) class with ALL/USAGE/MONITOR/OWNERSHIP. - PRIVS_FOR_RESOURCE_TYPE entry mapping the type to the priv class. - RESOURCE_SCOPES entry registering SchemaScope() (following the INTEGRATION precedent: no concrete resource class yet, just enough to let `priv: ... on: cortex search service ...` parse and render). - data_provider.list_grants: drop CREATE CORTEX SEARCH SERVICE from the "Skip undocumented privs" filter — it is documented (in SchemaPriv) and should round-trip via list_grants like any other schema priv. CANCEL QUERY remains in the skip list (genuinely unmodeled). - Unit tests covering the priv enum values and end-to-end Grant SQL rendering for both USAGE and MONITOR. Full Cortex Search Service resource modeling (CREATE ... AS , embedding model, target_lag, attributes) is left for a future PR — this change just covers grants. Co-Authored-By: Claude Opus 4.7 (1M context) --- snowcap/data_provider.py | 6 +++--- snowcap/enums.py | 1 + snowcap/privs.py | 8 ++++++++ snowcap/resources/resource.py | 7 +++++++ tests/test_grant.py | 26 ++++++++++++++++++++++++++ tests/test_privs.py | 24 ++++++++++++++++++++++++ 6 files changed, 69 insertions(+), 3 deletions(-) diff --git a/snowcap/data_provider.py b/snowcap/data_provider.py index 72bbd54..a14a596 100644 --- a/snowcap/data_provider.py +++ b/snowcap/data_provider.py @@ -3483,7 +3483,7 @@ def get_database_role_name_set() -> set[str]: continue # Skip undocumented privs - if data["privilege"] in ["CREATE CORTEX SEARCH SERVICE", "CANCEL QUERY"]: + if data["privilege"] in ["CANCEL QUERY"]: continue name = data["name"] @@ -3520,7 +3520,7 @@ def get_database_role_name_set() -> set[str]: continue # Skip undocumented privs - if data["privilege"] in ["CREATE CORTEX SEARCH SERVICE", "CANCEL QUERY"]: + if data["privilege"] in ["CANCEL QUERY"]: continue name = data["name"] @@ -3560,7 +3560,7 @@ def get_database_role_name_set() -> set[str]: continue # Skip undocumented privs - if data["privilege"] in ["CREATE CORTEX SEARCH SERVICE", "CANCEL QUERY"]: + if data["privilege"] in ["CANCEL QUERY"]: continue name = data["name"] diff --git a/snowcap/enums.py b/snowcap/enums.py index 3851194..3b12b9b 100644 --- a/snowcap/enums.py +++ b/snowcap/enums.py @@ -55,6 +55,7 @@ class ResourceType(ParseableEnum): CLASS = "CLASS" COLUMN = "COLUMN" COMPUTE_POOL = "COMPUTE POOL" + CORTEX_SEARCH_SERVICE = "CORTEX SEARCH SERVICE" DATABASE = "DATABASE" DATABASE_ROLE = "DATABASE ROLE" DATABASE_ROLE_GRANT = "DATABASE ROLE GRANT" diff --git a/snowcap/privs.py b/snowcap/privs.py index e6909ec..4d6ca8c 100644 --- a/snowcap/privs.py +++ b/snowcap/privs.py @@ -112,6 +112,13 @@ class DatabaseRolePriv(Priv): USAGE = "USAGE" +class CortexSearchServicePriv(Priv): + ALL = "ALL" + MONITOR = "MONITOR" + OWNERSHIP = "OWNERSHIP" + USAGE = "USAGE" + + class DirectoryTablePriv(Priv): OWNERSHIP = "OWNERSHIP" @@ -385,6 +392,7 @@ class WarehousePriv(Priv): ResourceType.CLASS: None, ResourceType.COLUMN: None, ResourceType.COMPUTE_POOL: None, + ResourceType.CORTEX_SEARCH_SERVICE: CortexSearchServicePriv, ResourceType.DATABASE_ROLE: DatabaseRolePriv, ResourceType.DATABASE: DatabasePriv, ResourceType.DIRECTORY_TABLE: DirectoryTablePriv, diff --git a/snowcap/resources/resource.py b/snowcap/resources/resource.py index 033277d..b27770f 100644 --- a/snowcap/resources/resource.py +++ b/snowcap/resources/resource.py @@ -287,6 +287,13 @@ def get_metadata(cls, field_name: str) -> ResourceSpecMetadata: # management still goes through the specific subtypes (APIIntegration, # CatalogIntegration, etc.). ResourceType.INTEGRATION: AccountScope(), + # CORTEX SEARCH SERVICE — Snowflake AI search service, schema-scoped. + # No concrete resource class yet (CREATE CORTEX SEARCH SERVICE involves an + # AS body and embedding model config that warrants its own PR). + # Registering a SchemaScope here lets users write + # `priv: USAGE on cortex search service ..` in YAML to + # manage access to services they create out-of-band (e.g. via dbt or DDL). + ResourceType.CORTEX_SEARCH_SERVICE: SchemaScope(), } diff --git a/tests/test_grant.py b/tests/test_grant.py index fa71c65..a32961b 100644 --- a/tests/test_grant.py +++ b/tests/test_grant.py @@ -148,6 +148,32 @@ def test_grant_on_dynamic_tables(): assert grant._data.on_type == ResourceType.DYNAMIC_TABLE +def test_grant_on_cortex_search_service(): + """USAGE/MONITOR on a CORTEX SEARCH SERVICE parses and renders correctly. + + Cortex Search is a schema-scoped service. Grants like + GRANT USAGE ON CORTEX SEARCH SERVICE .. TO ROLE r + let consuming roles query the service via SNOWFLAKE.CORTEX.SEARCH_PREVIEW; + MONITOR exposes get_ai_observability_events logs. + """ + grant = res.Grant( + priv="USAGE", + on_cortex_search_service="somedb.someschema.someservice", + to="somerole", + ) + assert grant._data.on == "SOMEDB.SOMESCHEMA.SOMESERVICE" + assert grant._data.on_type == ResourceType.CORTEX_SEARCH_SERVICE + assert "USAGE ON CORTEX SEARCH SERVICE" in grant.create_sql() + + monitor_grant = res.Grant( + priv="MONITOR", + on_cortex_search_service="somedb.someschema.someservice", + to="somerole", + ) + assert monitor_grant._data.on_type == ResourceType.CORTEX_SEARCH_SERVICE + assert "MONITOR ON CORTEX SEARCH SERVICE" in monitor_grant.create_sql() + + def test_grant_database_role_to_database_role(): database = res.Database(name="somedb") parent = res.DatabaseRole(name="parent", database=database) diff --git a/tests/test_privs.py b/tests/test_privs.py index 475fe02..6450721 100644 --- a/tests/test_privs.py +++ b/tests/test_privs.py @@ -7,6 +7,7 @@ Priv, AccountPriv, AlertPriv, + CortexSearchServicePriv, DatabasePriv, DatabaseRolePriv, DirectoryTablePriv, @@ -323,6 +324,29 @@ def test_usage_privilege(self): assert StagePriv.USAGE.value == "USAGE" +############################################################################# +# CortexSearchServicePriv Tests +############################################################################# + + +class TestCortexSearchServicePriv: + """Tests for CortexSearchServicePriv enum values (Snowflake Cortex Search).""" + + def test_all_privilege(self): + assert CortexSearchServicePriv.ALL.value == "ALL" + + def test_usage_privilege(self): + """USAGE is the priv needed to query a Cortex Search Service.""" + assert CortexSearchServicePriv.USAGE.value == "USAGE" + + def test_monitor_privilege(self): + """MONITOR is the priv needed for AI observability / request logs.""" + assert CortexSearchServicePriv.MONITOR.value == "MONITOR" + + def test_ownership_privilege(self): + assert CortexSearchServicePriv.OWNERSHIP.value == "OWNERSHIP" + + ############################################################################# # is_ownership_priv() Tests ############################################################################# From 07be3e416e94480580bd869c1377613c4d78ff0c Mon Sep 17 00:00:00 2001 From: Brandon Jackson Date: Sun, 17 May 2026 14:58:11 -0500 Subject: [PATCH 3/3] docs: add Cortex Search Service grant doc + USE AI FUNCTIONS example MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address documentation needs for the Cortex permission support added in this PR. Follows the pattern set by PR #16 (Iceberg REST Catalog Integration docs) per @noel's previous feedback — a resource-specific docs page with YAML + Python examples and a "Minimal example" section covering real-world gotchas. - docs/resources/cortex_search_service.md (new): grant patterns for CORTEX SEARCH SERVICE (USAGE, MONITOR), plus a complete worked example wiring up Cortex Code + Cortex AI SQL + Cortex Search access for a single role. Documents the PUBLIC-default behavior of CORTEX_USER and the explicit-grant requirement for COPILOT_USER, the database-role + account-priv layering, and the multi-grantee fetch prerequisite (this PR). - docs/resources/grant.md: add USE AI FUNCTIONS on ACCOUNT and USAGE/MONITOR on CORTEX SEARCH SERVICE to the Object Grants YAML example. - mkdocs.yml: register CortexSearchService under a new "AI" nav group alongside Access Control. Sets a place for future AI/Cortex resource pages (Cortex Search Service body modeling, semantic views, etc.) without bloating "Integrations" or "Access Control". Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/resources/cortex_search_service.md | 129 ++++++++++++++++++++++++ docs/resources/grant.md | 15 +++ mkdocs.yml | 2 + 3 files changed, 146 insertions(+) create mode 100644 docs/resources/cortex_search_service.md diff --git a/docs/resources/cortex_search_service.md b/docs/resources/cortex_search_service.md new file mode 100644 index 0000000..3dcc2e9 --- /dev/null +++ b/docs/resources/cortex_search_service.md @@ -0,0 +1,129 @@ +--- +description: >- + Grant USAGE / MONITOR on a Snowflake Cortex Search Service. +--- + +# CortexSearchService + +[Snowflake Documentation](https://docs.snowflake.com/en/user-guide/snowflake-cortex/cortex-search/cortex-search-overview) | Snowcap CLI label: `cortex_search_service` + +A Cortex Search Service is a schema-scoped Snowflake AI service that exposes +semantic + lexical search over a base table or view. Snowcap supports granting +access to existing services declaratively. The service itself (the +`CREATE CORTEX SEARCH SERVICE ... AS ` body, embedding model, +`target_lag`, attribute set, etc.) is **not** modeled as a concrete resource — +create it via DDL or dbt, then manage who can call it through `grants:`. + +## Examples + +### YAML + +```yaml +grants: + # Required to call SNOWFLAKE.CORTEX.SEARCH_PREVIEW() against the service. + - priv: USAGE + on: cortex search service somedb.someschema.transcript_search + to: customer_support + + # Required for get_ai_observability_events() / Cortex Search request logs. + - priv: MONITOR + on: cortex search service somedb.someschema.transcript_search + to: search_observability_role + + # Schema-scope privilege to allow a role to create new services. + - priv: CREATE CORTEX SEARCH SERVICE + on: schema somedb.someschema + to: search_author_role +``` + +### Python + +```python +# Grant USAGE +grant = Grant( + priv="USAGE", + on_cortex_search_service="somedb.someschema.transcript_search", + to="customer_support", +) + +# Grant MONITOR +grant = Grant( + priv="MONITOR", + on_cortex_search_service="somedb.someschema.transcript_search", + to="search_observability_role", +) +``` + +## Privileges + +| Privilege | Purpose | +|-------------|-------------------------------------------------------------------------| +| `USAGE` | Call `SNOWFLAKE.CORTEX.SEARCH_PREVIEW(...)` against the service. | +| `MONITOR` | Read request logs via `get_ai_observability_events(...)`. | +| `OWNERSHIP` | Standard ownership semantics — drop, alter, transfer. | +| `ALL` | Convenience: expand to all of the above. | + +The schema-scope privilege `CREATE CORTEX SEARCH SERVICE` is part of +[Grant](grant.md) under [SchemaPriv] — see the schema-privileges example +above. + +## Minimal example: full Cortex access for a developer role + +A common goal is "let this role use Cortex Code in Snowsight, call Cortex AI +SQL functions, and query our Cortex Search Service." Three pieces stack +together: + +```yaml +# 1. Account-level privilege for Cortex AI SQL (AI_COMPLETE, AI_FILTER, +# SUMMARIZE, embeddings, etc.). Granted to PUBLIC by default — declare it +# explicitly so access survives a future PUBLIC revoke. +grants: + - priv: USE AI FUNCTIONS + on: ACCOUNT + to: dbt_developer + + # 2. (Optional) USAGE on the search service itself + - priv: USAGE + on: cortex search service db_dev.cortex.faq_search + to: dbt_developer + +# 3. Database-role grants on the SNOWFLAKE shared database. COPILOT_USER is +# required for the Cortex Code pane in Snowsight. CORTEX_USER (or +# CORTEX_AGENT_USER) is required for Cortex AI SQL functions and Cortex +# Code's underlying calls. +database_role_grants: + - database_role: SNOWFLAKE.COPILOT_USER + roles: + - dbt_developer + - database_role: SNOWFLAKE.CORTEX_USER + roles: + - dbt_developer +``` + +### Gotchas + +- `SNOWFLAKE.CORTEX_USER` is granted to `PUBLIC` by default, so a role + inherits it transitively unless your account has revoked that default. + `SNOWFLAKE.COPILOT_USER` is **not** granted to `PUBLIC` — without an + explicit grant the Cortex Code pane is hidden in Snowsight. +- Declaring a `database_role_grants` entry for a role that is already + granted to another grantee (e.g. `ACCOUNTADMIN`) requires snowcap ≥ the + release containing the multi-grantee fetch fix. Earlier versions emit a + spurious `UpdateResource(to_role: ACCOUNTADMIN → )` diff instead of + a clean create. +- `USE AI FUNCTIONS ON ACCOUNT` is the account privilege, separate from + the `SNOWFLAKE.CORTEX_USER` database role. Both are typically required + for Cortex AI SQL calls; missing either produces a + "Function requires X privilege" error at runtime. +- Querying a search service also requires `USAGE` on its parent database + and schema. If those are absent the call fails before reaching the + service-level USAGE check. + +## See also + +- [Snowflake — Cortex Search overview](https://docs.snowflake.com/en/user-guide/snowflake-cortex/cortex-search/cortex-search-overview) +- [Snowflake — Cortex Code access control](https://docs.snowflake.com/en/user-guide/cortex-code/cortex-code-snowsight#access-control-requirements) +- [Snowflake — Cortex AI SQL required privileges](https://docs.snowflake.com/en/user-guide/snowflake-cortex/aisql#required-privileges) +- [Snowflake — Cortex Search Monitor / logs](https://docs.snowflake.com/en/user-guide/snowflake-cortex/cortex-search/cortex-search-monitor) +- [DatabaseRole](database_role.md) — for granting `SNOWFLAKE.*` database roles +- [Grant](grant.md) — for the underlying grant resource and YAML schema diff --git a/docs/resources/grant.md b/docs/resources/grant.md index 34215db..81dad6d 100644 --- a/docs/resources/grant.md +++ b/docs/resources/grant.md @@ -44,6 +44,21 @@ grants: - priv: USAGE on: warehouse some_warehouse to: some_role + + # AI: account-level privilege for Cortex AI SQL functions + - priv: USE AI FUNCTIONS + on: ACCOUNT + to: cortex_user_role + + # AI: USAGE on a Cortex Search Service to call SNOWFLAKE.CORTEX.SEARCH_PREVIEW + - priv: USAGE + on: cortex search service somedb.someschema.someservice + to: search_consumer_role + + # AI: MONITOR on a Cortex Search Service to query observability logs + - priv: MONITOR + on: cortex search service somedb.someschema.someservice + to: search_observability_role ``` #### Future Grants diff --git a/mkdocs.yml b/mkdocs.yml index 30170bb..888a98d 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -115,6 +115,8 @@ nav: - Role: resources/role.md - RoleGrant: resources/role_grant.md - User: resources/user.md + - AI: + - CortexSearchService: resources/cortex_search_service.md - Account & Monitoring: - AccountParameter: resources/account_parameter.md - EventTable: resources/event_table.md