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
88 changes: 88 additions & 0 deletions docs/resources/dbt_project.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
---
description: >-
Grant USAGE / MONITOR on a Snowflake dbt project object (dbt Projects on Snowflake).
---

# DbtProject

[Snowflake Documentation](https://docs.snowflake.com/en/user-guide/data-engineering/dbt-projects-on-snowflake-access-control) | Snowcap CLI label: `dbt_project`

A dbt project object (part of *dbt Projects on Snowflake*) is a schema-scoped
object that packages a dbt project so it can be executed with `EXECUTE DBT
PROJECT`. Snowcap supports granting access to existing dbt project objects
declaratively. The object itself (`CREATE DBT PROJECT ... FROM <workspace|stage>`
with its profiles/target config) is **not** modeled as a concrete resource —
create it via a Snowflake Workspace or DDL, then manage who can execute and
monitor it through `grants:`.

## Examples

### YAML

```yaml
grants:
# Required to EXECUTE DBT PROJECT and to list/retrieve the project's files.
- priv: USAGE
on: dbt project somedb.someschema.analytics_dbt
to: transformer_role

# Required to view the project (details + run history) in Snowsight.
- priv: MONITOR
on: dbt project somedb.someschema.analytics_dbt
to: analytics_observer

# Schema-scope privilege to allow a role to create new dbt project objects
# (e.g. deploying from a Workspace).
- priv: CREATE DBT PROJECT
on: schema somedb.someschema
to: dbt_author_role
```

### Python

```python
# Grant USAGE (execute the project + read its files)
grant = Grant(
priv="USAGE",
on_dbt_project="somedb.someschema.analytics_dbt",
to="transformer_role",
)

# Grant MONITOR (Snowsight project details + run history)
grant = Grant(
priv="MONITOR",
on_dbt_project="somedb.someschema.analytics_dbt",
to="analytics_observer",
)
```

## Privileges

| Privilege | Purpose |
|--------------------|---------------------------------------------------------------------------------------------|
| `USAGE` | Execute the dbt project (`EXECUTE DBT PROJECT`) and list/retrieve its files. |
| `MONITOR` | View the project's details and run history in Snowsight. |
| `OWNERSHIP` | Full control of the object. Exclusive to a single role (like tasks). |
| `ALL` | All privileges above. |

The schema-scope privilege `CREATE DBT PROJECT` (see [Schema](#) grants) lets a
role create dbt project objects in that schema.

## Minimal example

Two roles, least privilege: one runs the project, one only watches it in
Snowsight. Neither owns it — ownership stays with the deploying role.

```yaml
grants:
- priv: USAGE
on: dbt project analytics.transforms.daily_models
to: dbt_runner
- priv: MONITOR
on: dbt project analytics.transforms.daily_models
to: analytics_viewer
```

> Note: in a managed-access schema, granting these privileges is restricted to
> the schema owner or a role with `MANAGE GRANTS` — the same constraint that
> applies to tasks.
10 changes: 10 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

# Data Engineering: USAGE on a dbt project object to EXECUTE DBT PROJECT
- priv: USAGE
on: dbt project somedb.someschema.analytics_dbt
to: transformer_role

# Data Engineering: MONITOR on a dbt project for Snowsight run history
- priv: MONITOR
on: dbt project somedb.someschema.analytics_dbt
to: analytics_observer
```

#### Future Grants
Expand Down
2 changes: 2 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,8 @@ nav:
- ResourceMonitor: resources/resource_monitor.md
- Apps:
- Streamlit: resources/streamlit.md
- Data Engineering:
- DbtProject: resources/dbt_project.md
- Data Loading:
- File Formats:
- JSONFileFormat: resources/json_file_format.md
Expand Down
15 changes: 15 additions & 0 deletions snowcap/data_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -1622,6 +1622,21 @@ def fetch_database_role_grant(session: SnowflakeConnection, fqn: FQN):
}


def fetch_dbt_project(session: SnowflakeConnection, fqn: FQN):
projects = _show_resources(session, "DBT PROJECTS", fqn)
if len(projects) == 0:
return None
if len(projects) > 1:
raise Exception(f"Found multiple dbt projects matching {fqn}")

data = projects[0]
return {
"name": data["name"],
"owner": _get_owner_identifier(data),
"comment": data["comment"] or None,
}


def fetch_dynamic_table(session: SnowflakeConnection, fqn: FQN):
show_result = _show_resources(session, "DYNAMIC TABLES", fqn)
if len(show_result) == 0:
Expand Down
1 change: 1 addition & 0 deletions snowcap/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ class ResourceType(ParseableEnum):
DATABASE = "DATABASE"
DATABASE_ROLE = "DATABASE ROLE"
DATABASE_ROLE_GRANT = "DATABASE ROLE GRANT"
DBT_PROJECT = "DBT PROJECT"
DIRECTORY_TABLE = "DIRECTORY TABLE"
DYNAMIC_TABLE = "DYNAMIC TABLE"
EVENT_TABLE = "EVENT TABLE"
Expand Down
9 changes: 9 additions & 0 deletions snowcap/privs.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,13 @@ class CortexSearchServicePriv(Priv):
USAGE = "USAGE"


class DbtProjectPriv(Priv):
ALL = "ALL"
MONITOR = "MONITOR"
OWNERSHIP = "OWNERSHIP"
USAGE = "USAGE"


class DirectoryTablePriv(Priv):
OWNERSHIP = "OWNERSHIP"

Expand Down Expand Up @@ -240,6 +247,7 @@ class SchemaPriv(Priv):
CREATE_AUTHENTICATION_POLICY = "CREATE AUTHENTICATION POLICY"
CREATE_CORTEX_SEARCH_SERVICE = "CREATE CORTEX SEARCH SERVICE"
CREATE_DATASET = "CREATE DATASET"
CREATE_DBT_PROJECT = "CREATE DBT PROJECT"
CREATE_DYNAMIC_TABLE = "CREATE DYNAMIC TABLE"
CREATE_EVENT_TABLE = "CREATE EVENT TABLE"
CREATE_EXTERNAL_TABLE = "CREATE EXTERNAL TABLE"
Expand Down Expand Up @@ -395,6 +403,7 @@ class WarehousePriv(Priv):
ResourceType.CORTEX_SEARCH_SERVICE: CortexSearchServicePriv,
ResourceType.DATABASE_ROLE: DatabaseRolePriv,
ResourceType.DATABASE: DatabasePriv,
ResourceType.DBT_PROJECT: DbtProjectPriv,
ResourceType.DIRECTORY_TABLE: DirectoryTablePriv,
ResourceType.DYNAMIC_TABLE: TablePriv,
ResourceType.EVENT_TABLE: EventTablePriv,
Expand Down
6 changes: 6 additions & 0 deletions snowcap/resources/resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,12 @@ 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(),
# DBT PROJECT — dbt Projects on Snowflake, schema-scoped. No concrete
# resource class yet (CREATE DBT PROJECT wraps a workspace/git source +
# profiles config that warrants its own PR). Registering a SchemaScope here
# lets users write `priv: USAGE on dbt project <db>.<schema>.<name>` in YAML
# to manage access to projects they create out-of-band (Workspaces / DDL).
ResourceType.DBT_PROJECT: SchemaScope(),
}


Expand Down
27 changes: 27 additions & 0 deletions tests/test_grant.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,33 @@ def test_grant_on_cortex_search_service():
assert "MONITOR ON CORTEX SEARCH SERVICE" in monitor_grant.create_sql()


def test_grant_on_dbt_project():
"""USAGE/MONITOR on a DBT PROJECT parses and renders correctly.

A dbt project object (dbt Projects on Snowflake) is schema-scoped. USAGE
lets a role execute the project and list/retrieve its files; MONITOR
exposes project details + run history in Snowsight:
GRANT USAGE ON DBT PROJECT <db>.<schema>.<proj> TO ROLE r
GRANT MONITOR ON DBT PROJECT <db>.<schema>.<proj> TO ROLE observer
"""
grant = res.Grant(
priv="USAGE",
on_dbt_project="somedb.someschema.someproject",
to="somerole",
)
assert grant._data.on == "SOMEDB.SOMESCHEMA.SOMEPROJECT"
assert grant._data.on_type == ResourceType.DBT_PROJECT
assert "USAGE ON DBT PROJECT" in grant.create_sql()

monitor_grant = res.Grant(
priv="MONITOR",
on_dbt_project="somedb.someschema.someproject",
to="observer",
)
assert monitor_grant._data.on_type == ResourceType.DBT_PROJECT
assert "MONITOR ON DBT PROJECT" 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)
Expand Down
24 changes: 24 additions & 0 deletions tests/test_privs.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
CortexSearchServicePriv,
DatabasePriv,
DatabaseRolePriv,
DbtProjectPriv,
DirectoryTablePriv,
EventTablePriv,
ExternalVolumePriv,
Expand Down Expand Up @@ -347,6 +348,29 @@ def test_ownership_privilege(self):
assert CortexSearchServicePriv.OWNERSHIP.value == "OWNERSHIP"


#############################################################################
# DbtProjectPriv Tests
#############################################################################


class TestDbtProjectPriv:
"""Tests for DbtProjectPriv enum values (dbt Projects on Snowflake)."""

def test_all_privilege(self):
assert DbtProjectPriv.ALL.value == "ALL"

def test_usage_privilege(self):
"""USAGE is the priv needed to execute a dbt project + list its files."""
assert DbtProjectPriv.USAGE.value == "USAGE"

def test_monitor_privilege(self):
"""MONITOR is the priv needed for project details + run history."""
assert DbtProjectPriv.MONITOR.value == "MONITOR"

def test_ownership_privilege(self):
assert DbtProjectPriv.OWNERSHIP.value == "OWNERSHIP"


#############################################################################
# is_ownership_priv() Tests
#############################################################################
Expand Down
Loading