Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions docs/resources/grant.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,16 @@ grants:
- priv: MONITOR
on: cortex search service somedb.someschema.someservice
to: search_observability_role

# AI: schema-scope privilege to allow a role to create semantic views
- priv: CREATE SEMANTIC VIEW
on: schema somedb.someschema
to: semantic_view_author_role

# AI: SELECT on a Semantic View to query it via Cortex Analyst
- priv: SELECT
on: semantic view somedb.someschema.somesv
to: semantic_view_consumer_role
```

#### Future Grants
Expand All @@ -77,6 +87,11 @@ grants:
- future tables in schema someschema
- future views in schema someschema
to: somerole

# AI: future semantic views in schema
- priv: SELECT
on: future semantic views in schema somedb.someschema
to: somerole
```

#### Grants on All Resources
Expand All @@ -95,6 +110,11 @@ grants:
- all tables in schema someschema
- all views in schema someschema
to: somerole

# AI: all semantic views in schema
- priv: SELECT
on: all semantic views in schema somedb.someschema
to: somerole
```

### Python
Expand Down Expand Up @@ -185,6 +205,7 @@ grant_on_all = Grant(
- `"schema my_db.my_schema"` - for schema privileges
- `"warehouse my_wh"` - for warehouse privileges
- `"database my_db"` - for database privileges
- `"semantic view my_db.my_schema.my_sv"` - for semantic view privileges
- `"future tables in schema my_schema"` - for future grants
- `"all tables in database my_db"` - for grants on all existing objects

Expand Down
88 changes: 88 additions & 0 deletions docs/resources/semantic_view.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
---
description: >-
Grant SELECT / REFERENCES / MONITOR on a Snowflake Semantic View.
---

# SemanticView

[Snowflake Documentation](https://docs.snowflake.com/en/user-guide/views-semantic/overview) | Snowcap CLI label: `semantic_view`

A Semantic View is a schema-scoped Snowflake object that defines business
metrics, dimensions, and relationships over one or more tables, used by
Cortex Analyst and other consumers to query data in business terms. Snowcap
supports granting access to existing semantic views declaratively. The
semantic view itself (the `CREATE SEMANTIC VIEW ... TABLES (...) FACTS (...)
DIMENSIONS (...) METRICS (...)` body) is **not** modeled as a concrete
resource — create it via DDL or dbt, then manage who can query it through
`grants:`.

## Examples

### YAML

```yaml
grants:
# Required to query the semantic view (e.g. via Cortex Analyst).
- priv: SELECT
on: semantic view somedb.someschema.sales_metrics
to: analyst_role

# Required to inspect the semantic view's structure via the information schema.
- priv: REFERENCES
on: semantic view somedb.someschema.sales_metrics
to: analyst_role

# Required for get_ai_observability_events() / Cortex Analyst request logs.
- priv: MONITOR
on: semantic view somedb.someschema.sales_metrics
to: search_observability_role

# Schema-scope privilege to allow a role to create new semantic views.
- priv: CREATE SEMANTIC VIEW
on: schema somedb.someschema
to: semantic_view_author_role
```

### Python

```python
# Grant SELECT
grant = Grant(
priv="SELECT",
on_semantic_view="somedb.someschema.sales_metrics",
to="analyst_role",
)

# Grant REFERENCES
grant = Grant(
priv="REFERENCES",
on_semantic_view="somedb.someschema.sales_metrics",
to="analyst_role",
)

# Grant MONITOR
grant = Grant(
priv="MONITOR",
on_semantic_view="somedb.someschema.sales_metrics",
to="search_observability_role",
)
```

## Privileges

| Privilege | Purpose |
|--------------|---------------------------------------------------------------------------|
| `SELECT` | Query the semantic view — sufficient on its own, without `SELECT` on the underlying tables. |
| `REFERENCES` | Inspect the semantic view's structure via the information schema. |
| `MONITOR` | Read Cortex Analyst 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 SEMANTIC VIEW` is part of
[Grant](grant.md) under `SchemaPriv` — see the schema-privileges example
above.

## See also

- [Snowflake — Semantic views overview](https://docs.snowflake.com/en/user-guide/views-semantic/overview)
- [Grant](grant.md) — for the underlying grant resource and YAML schema
6 changes: 6 additions & 0 deletions docs/snowflake-permissions.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,12 @@ GRANT CREATE COMPUTE POOL ON ACCOUNT TO ROLE SNOWCAP_ADMIN;
GRANT IMPORTED PRIVILEGES ON DATABASE SNOWFLAKE TO ROLE SNOWCAP_ADMIN;
```

Unlike the account-level privileges above, `CREATE SEMANTIC VIEW` is schema-scoped — grant it per schema where your configuration manages semantic view grants:

```sql
GRANT CREATE SEMANTIC VIEW ON SCHEMA somedb.someschema TO ROLE SNOWCAP_ADMIN;
```

See [Optimizing Grant Fetching with ACCOUNT_USAGE](getting-started.md#optimizing-grant-fetching-with-account_usage) for when the ACCOUNT_USAGE optimization is worth enabling.

### Step 3: Create the service user
Expand Down
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ nav:
- User: resources/user.md
- AI:
- CortexSearchService: resources/cortex_search_service.md
- SemanticView: resources/semantic_view.md
- Account & Monitoring:
- AccountParameter: resources/account_parameter.md
- EventTable: resources/event_table.md
Expand Down
1 change: 1 addition & 0 deletions snowcap/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ class ResourceType(ParseableEnum):
SCHEMA = "SCHEMA"
SECRET = "SECRET"
SECURITY_INTEGRATION = "SECURITY INTEGRATION"
SEMANTIC_VIEW = "SEMANTIC VIEW"
SEQUENCE = "SEQUENCE"
SERVICE = "SERVICE"
SHARE = "SHARE"
Expand Down
5 changes: 4 additions & 1 deletion snowcap/lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -595,10 +595,13 @@ def drop_grant(urn: URN, data: dict, **kwargs):
"REVOKE",
data["priv"],
"ON ALL",
data["items_type"],
pluralize(data["items_type"]).upper(),
"IN",
data["on_type"],
data["on"],
"FROM",
data["to_type"],
data["to"],
)
else:
return tidy_sql(
Expand Down
10 changes: 10 additions & 0 deletions snowcap/privs.py
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,7 @@ class SchemaPriv(Priv):
CREATE_RESOURCE_GROUP = "CREATE RESOURCE GROUP"
CREATE_ROW_ACCESS_POLICY = "CREATE ROW ACCESS POLICY"
CREATE_SECRET = "CREATE SECRET"
CREATE_SEMANTIC_VIEW = "CREATE SEMANTIC VIEW"
CREATE_SEQUENCE = "CREATE SEQUENCE"
CREATE_SERVICE = "CREATE SERVICE"
CREATE_SERVICE_CLASS = "CREATE SERVICE CLASS"
Expand All @@ -286,6 +287,14 @@ class SecretPriv(Priv):
USAGE = "USAGE"


class SemanticViewPriv(Priv):
ALL = "ALL"
MONITOR = "MONITOR"
OWNERSHIP = "OWNERSHIP"
REFERENCES = "REFERENCES"
SELECT = "SELECT"


class GitRepositoryPriv(Priv):
OWNERSHIP = "OWNERSHIP"
READ = "READ"
Expand Down Expand Up @@ -427,6 +436,7 @@ class WarehousePriv(Priv):
ResourceType.SCHEMA: SchemaPriv,
ResourceType.SECRET: SecretPriv,
ResourceType.SECURITY_INTEGRATION: IntegrationPriv,
ResourceType.SEMANTIC_VIEW: SemanticViewPriv,
ResourceType.SEQUENCE: SequencePriv,
ResourceType.SERVICE: None,
ResourceType.SHARE: None,
Expand Down
7 changes: 7 additions & 0 deletions snowcap/resources/resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,13 @@ def get_metadata(cls, field_name: str) -> ResourceSpecMetadata:
# `priv: USAGE on cortex search service <db>.<schema>.<name>` in YAML to
# manage access to services they create out-of-band (e.g. via dbt or DDL).
ResourceType.CORTEX_SEARCH_SERVICE: SchemaScope(),
# SEMANTIC VIEW — Snowflake semantic layer object, schema-scoped.
# No concrete resource class yet (CREATE SEMANTIC VIEW involves tables,
# relationships, facts, and metrics that warrant its own PR).
# Registering a SchemaScope here lets users write
# `priv: SELECT on semantic view <db>.<schema>.<name>` in YAML to
# manage access to semantic views they create out-of-band (e.g. via dbt or DDL).
ResourceType.SEMANTIC_VIEW: SchemaScope(),
}


Expand Down
1 change: 1 addition & 0 deletions tests/fixtures/sql/grant.sql
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,6 @@ GRANT CREATE MATERIALIZED VIEW ON SCHEMA mydb.myschema TO ROLE myrole;
GRANT ALL PRIVILEGES ON FUNCTION mydb.myschema.add5(number) TO ROLE analyst;
GRANT ALL PRIVILEGES ON FUNCTION mydb.myschema.add5(string) TO ROLE analyst;
GRANT USAGE ON PROCEDURE mydb.myschema.myprocedure(number) TO ROLE analyst;
GRANT SELECT ON SEMANTIC VIEW mydb.myschema.mysemanticview TO ROLE analyst;


79 changes: 78 additions & 1 deletion tests/integration/test_grant_patterns.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@
import os

import pytest
import snowflake.connector.errors

from tests.helpers import safe_fetch
from snowcap import resources as res
from snowcap.blueprint import Blueprint, CreateResource
from snowcap.client import reset_cache
from snowcap.client import FEATURE_NOT_ENABLED_ERR, UNSUPPORTED_FEATURE, reset_cache
from snowcap.gitops import collect_blueprint_config

TEST_ROLE = os.environ.get("TEST_SNOWFLAKE_ROLE")
Expand Down Expand Up @@ -327,6 +328,82 @@ def test_role_grant_multiple_roles_to_single_role(self, cursor, suffix, marked_f
assert grant_data is not None, f"Role grant {source_name} -> {target_name} was not created"


class TestSemanticViewGrantsIntegration:
"""Grant fetch round-trip for SEMANTIC VIEW.

SHOW GRANTS returns granted_on='SEMANTIC_VIEW' for a live semantic-view
grant; whether that string maps to ResourceType.SEMANTIC_VIEW (via
fetch_role_privileges' granted_on.replace('_', ' ') normalization) can
only be verified against a real account. This test proves that mapping,
plus that ALL/FUTURE SEMANTIC VIEWS IN SCHEMA grants execute successfully.
"""

def test_semantic_view_grant_fetch_round_trip(self, cursor, suffix, test_db, marked_for_cleanup):
"""Test: SELECT + FUTURE SEMANTIC VIEWS IN SCHEMA grants fetch back with no drift."""
session = cursor.connection

schema_name = f"SV_GRANT_SCHEMA_{suffix}"
schema = res.Schema(name=f"{test_db}.{schema_name}")
cursor.execute(schema.create_sql(if_not_exists=True))
marked_for_cleanup.append(schema)

table_fqn = f"{test_db}.{schema_name}.SV_SOURCE_TABLE_{suffix}"
cursor.execute(f"CREATE TABLE {table_fqn} (id INT, name STRING)")

sv_fqn = f"{test_db}.{schema_name}.SV_{suffix}"
try:
cursor.execute(f"CREATE SEMANTIC VIEW {sv_fqn} TABLES (t AS {table_fqn} PRIMARY KEY (id))")
except snowflake.connector.errors.ProgrammingError as err:
if err.errno in (UNSUPPORTED_FEATURE, FEATURE_NOT_ENABLED_ERR):
pytest.skip("SEMANTIC VIEW is not supported on this account")
raise

# Create a role to receive grants
role_name = f"SV_GRANT_ROLE_{suffix}"
role = res.Role(name=role_name)
cursor.execute(role.create_sql(if_not_exists=True))
marked_for_cleanup.append(role)

# Grant SELECT on the specific semantic view, plus a FUTURE grant on the schema
yaml_config = {
"grants": [
{
"priv": "SELECT",
"on_semantic_view": sv_fqn,
"to_role": role_name,
},
{
"priv": "SELECT",
"on": f"FUTURE SEMANTIC VIEWS IN SCHEMA {test_db}.{schema_name}",
"to_role": role_name,
},
],
}

bc = collect_blueprint_config(yaml_config)
blueprint = Blueprint.from_config(bc)

plan = blueprint.plan(session)
assert len(plan) == 2
assert all(isinstance(p, CreateResource) for p in plan)

blueprint.apply(session, plan)

# Verify the SELECT ON SEMANTIC VIEW grant fetches back and maps to ResourceType.SEMANTIC_VIEW
select_grant = res.Grant(priv="SELECT", on_semantic_view=sv_fqn, to=role)
assert select_grant.urn.resource_type.value == "SEMANTIC VIEW"
fetched = safe_fetch(cursor, select_grant.urn)
assert fetched is not None, "SELECT ON SEMANTIC VIEW grant was not created"
assert fetched["priv"] == "SELECT"

# Re-plan should show zero drift for both grants
reset_cache()
bc2 = collect_blueprint_config(yaml_config)
blueprint2 = Blueprint.from_config(bc2)
plan2 = blueprint2.plan(session)
assert len(plan2) == 0, f"Expected no drift but got: {plan2}"


class TestGrantCleanupIntegration:
"""Test that grant cleanup works correctly."""

Expand Down
Loading
Loading