Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
323ba13
feat(grants): warn when database-level future grants are shadowed or …
claude Aug 15, 2026
bff6e8d
fix(grants): tolerate inherited grants in remote state
claude Aug 15, 2026
54f0744
ci: move workflow actions off deprecated Node 20 runtimes
claude Aug 15, 2026
26202e4
style: make formatting and spelling checks enforceable
claude Aug 15, 2026
fc729c5
feat(grants): support Snowflake inherited grants
claude Aug 15, 2026
b5299fb
feat(grants): let config enable the inherited grants preview itself
claude Aug 15, 2026
72b5d55
docs(grants): correct what Snowcap can and cannot enable
claude Aug 15, 2026
5403d77
fix(grants): treat IMPORTED PRIVILEGES fan-out as covered by its grant
claude Aug 15, 2026
7aae7e0
feat(grants): support CORTEX AGENT SERVER object type
claude Aug 15, 2026
c5e3a23
test(grants): cover the remote-state path that hit the agent server type
claude Aug 16, 2026
d4a67be
fix(grants): treat CORTEX AGENT SERVER as a synonym of MCP SERVER
claude Aug 16, 2026
c426540
feat(gitops): add a where filter to for_each
claude Aug 16, 2026
bf5b41e
docs(rbac): document the cloned-database grant pattern
claude Aug 16, 2026
c454996
Create resources inside a container this plan transfers as its new owner
claude Aug 16, 2026
cc68038
Stop listing database role grants as object grants
claude Aug 16, 2026
a68779b
Revoke the share when dropping a grant on a shared database
claude Aug 16, 2026
8488b49
Identify grants by the object name their DDL uses, not the one SHOW G…
claude Aug 16, 2026
1f59841
Revoke account-level privileges as the system role that owns them
claude Aug 16, 2026
43fdd31
Manage grants held by a database role as the role that owns the database
claude Aug 16, 2026
29da904
Report drops Snowflake accepted without carrying out
claude Aug 16, 2026
92110a6
Revert "Manage grants held by a database role as the role that owns t…
claude Aug 16, 2026
2f6d59b
Reapply "Manage grants held by a database role as the role that owns …
claude Aug 16, 2026
d78c9c1
Do not list the usage a database role is born with
claude Aug 16, 2026
02d0d9b
Let YAML grant a database role to another database role
claude Aug 16, 2026
a28979c
Read future grants when syncing, and identify grants the way config d…
claude Aug 17, 2026
6c86423
style: ruff format three test files to satisfy enforced format check
noel Aug 17, 2026
ff41668
Merge remote-tracking branch 'origin/main' into pr58-review
noel Aug 17, 2026
05d8a01
fix: address multi-agent review findings on inherited-grants PR
noel Aug 17, 2026
056fa17
fix(types): annotate surviving_drops survivors as list[ResourceChange]
noel Aug 17, 2026
6978f22
fix: address full-branch review findings on grant-sync PR
noel Aug 17, 2026
9422b79
fix(grants): normalize synonym object types on the future-grant fetch…
noel Aug 17, 2026
1222245
fix: persist plan levels for apply --plan; ALL-grant coverage; quoted…
noel Aug 17, 2026
42ceee8
fix(#10 diagnostic) + ponytail: actionable db-role survivor guidance;…
noel Aug 17, 2026
b4e1020
fix: close re-review gaps in the earlier fixes
noel Aug 17, 2026
ed2c4a9
fix(grants): parse 3-word collection types; drop dead limit knob and …
noel Aug 17, 2026
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
6 changes: 4 additions & 2 deletions .github/workflows/issue-triage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ jobs:
# silently. The `comment` job below keeps its own ISSUE/REPO — it needs them.
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5

- name: Clear the agent→workflow handoff file
run: rm -f "$RUNNER_TEMP/triage.md"
Expand Down Expand Up @@ -163,7 +163,7 @@ jobs:

- name: Upload triage note
if: always()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: issue-triage-note
path: ${{ runner.temp }}/triage.md
Expand All @@ -186,6 +186,8 @@ jobs:
REPO: ${{ github.repository }}
steps:
- name: Download triage note
# Pinned to v4 deliberately: actions/download-artifact has no Node 24
# release yet, so a bump would not clear the deprecation warning.
uses: actions/download-artifact@v4
continue-on-error: true
with:
Expand Down
6 changes: 4 additions & 2 deletions .github/workflows/pr-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ jobs:
env:
BASE_REF: ${{ github.event.pull_request.base.ref }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
fetch-depth: 0 # full history so the base...head diff is available

Expand Down Expand Up @@ -107,7 +107,7 @@ jobs:

- name: Upload findings
if: always()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: pr-review-findings
path: ${{ runner.temp }}/review.md
Expand All @@ -134,6 +134,8 @@ jobs:
REPO: ${{ github.repository }}
steps:
- name: Download findings
# Pinned to v4 deliberately: actions/download-artifact has no Node 24
# release yet, so a bump would not clear the deprecation warning.
uses: actions/download-artifact@v4
continue-on-error: true
with:
Expand Down
6 changes: 4 additions & 2 deletions .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,12 @@ jobs:
- name: Run checks (linter, code style, static type checks, tests)
run: |
source ./.venv/bin/activate
ruff check snowcap/
make lint
make typecheck
python -m pytest --ignore=tests/integration --cov=snowcap --cov-report=xml
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v5
# v6 is the first release that pins a Node 24 build of actions/github-script;
# v5 pins v7.0.1, which GitHub now reports as a deprecated Node 20 action.
uses: codecov/codecov-action@v6
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
8 changes: 7 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: install install-dev test integration style check clean build docs coverage provision-test-account drop-test-account
.PHONY: install install-dev test integration style lint check clean build docs coverage provision-test-account drop-test-account
EDITION ?= standard or enterprise
EMAIL ?=

Expand Down Expand Up @@ -35,6 +35,12 @@ style:
python -m black .
codespell .

# Same checks as `style`, but read-only. This is what CI runs.
lint:
python -m black --check .
codespell .
ruff check snowcap/


typecheck:
mypy --exclude="snowcap/resources/.*" --exclude="snowcap/sql.py" --follow-imports=skip snowcap/
Expand Down
44 changes: 44 additions & 0 deletions docs/resources/database_role_grant.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
---
description: >-
A database role grant in Snowflake.
---

# DatabaseRoleGrant

[Snowflake Documentation](https://docs.snowflake.com/en/sql-reference/sql/grant-database-role) | Snowcap CLI label: `database_role_grant`

Represents a grant of a database role to another role or database role in Snowflake.


## Examples

### Python

```python
# Grant to Database Role:
role_grant = DatabaseRoleGrant(database_role="somedb.somerole", to_database_role="somedb.someotherrole")
role_grant = DatabaseRoleGrant(database_role="somedb.somerole", to=DatabaseRole(database="somedb", name="someotherrole"))
# Grant to Role:
role_grant = DatabaseRoleGrant(database_role="somedb.somerole", to_role="somerole")
role_grant = DatabaseRoleGrant(database_role="somedb.somerole", to=Role(name="somerole"))
```


### YAML

```yaml
database_role_grants:
- database_role: somedb.somerole
to_database_role: somedb.someotherrole
- database_role: somedb.somerole
to_role: somerole
```


## Fields

* `database_role` (string or [Role](role.md), required) - The database role to be granted.
* `to_role` (string or [Role](role.md)) - The role to which the database role is granted.
* `to_database_role` (string or [User](user.md)) - The database role to which the database role is granted.


101 changes: 98 additions & 3 deletions docs/resources/grant.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ description: >-

[Snowflake Documentation](https://docs.snowflake.com/en/sql-reference/sql/grant-privilege) | Snowcap CLI label: `grant`

The `Grant` resource represents a privilege grant, a future grant, or a grant of privileges on all resources of a specified type to a role in Snowflake.
The `Grant` resource represents a privilege grant, a future grant, an inherited grant, or a grant of privileges on all resources of a specified type to a role in Snowflake.

## Examples

Expand Down Expand Up @@ -85,7 +85,9 @@ grants:
on: dbt project somedb.someschema.analytics_dbt
to: analytics_observer

# AI: USAGE on an MCP Server so MCP clients can call its tools
# AI: USAGE on an MCP Server so MCP clients can call its tools.
# Snowflake reports grants on these with granted_on 'CORTEX_AGENT_SERVER',
# which Snowcap accepts as a synonym; the DDL grammar only takes MCP SERVER.
- priv: USAGE
on: mcp server somedb.someschema.someserver
to: mcp_client_role
Expand Down Expand Up @@ -156,6 +158,42 @@ grants:
to: somerole
```

#### Inherited Grants

An inherited grant is a single grant on a container that covers every current **and
future** object of a type inside it, replacing an `all` + `future` pair.

```yaml
grants:
- priv: SELECT
on: inherited tables in schema somedb.someschema
to: somerole

# Multiple privileges expand to one statement each
- priv:
- SELECT
- INSERT
on: inherited tables in database somedb
to: somerole

# The account can only be the container of an inherited grant
- priv: SELECT
on: inherited tables in account
to: somerole

# Or turn a grant on all objects into an inherited one
- priv: SELECT
on: all tables in database somedb
inherited: true
to: somerole

# Delegate to a role holding MANAGE GRANTS on the container
- priv: SELECT
on: inherited tables in database sales_db
to: analyst
owner: sales_db_admin
```

### Python

#### Object Grants
Expand Down Expand Up @@ -242,6 +280,32 @@ grant_on_all = Grant(
)
```

#### Inherited Grants

```python
inherited_grant = Grant(
priv="SELECT",
on="INHERITED TABLES IN SCHEMA somedb.someschema",
to="somerole",
)
inherited_grant = Grant(
priv="SELECT",
on=["INHERITED", "TABLES", Database(name="somedb")],
to="somerole",
)

# The account can only be the container of an inherited grant
inherited_grant = Grant(priv="SELECT", on="INHERITED TABLES IN ACCOUNT", to="somerole")

# Or turn a grant on all objects into an inherited one
inherited_grant = Grant(
priv="SELECT",
on="ALL TABLES IN DATABASE somedb",
inherited=True,
to="somerole",
)
```

## Fields

- **`priv`** (`string` or `list`, required):
Expand All @@ -260,6 +324,8 @@ grant_on_all = Grant(
- `"service my_db.my_schema.my_service"` - for service privileges
- `"future tables in schema my_schema"` - for future grants
- `"all tables in database my_db"` - for grants on all existing objects
- `"inherited tables in database my_db"` - for inherited grants, covering existing and future objects
- `"inherited tables in account"` - inherited grants are the only kind that can be scoped to the account

- **`to`** (`string` or [Role](role.md), required):
The role to which the privileges are granted.
Expand All @@ -268,10 +334,39 @@ grant_on_all = Grant(
Specifies whether the grantee can grant the privileges to other roles. Defaults to `false`.

- **`owner`** (`string` or [Role](role.md), optional):
The owner role of the grant. Defaults to `"SYSADMIN"`.
The owner role of the grant. Defaults to `"SYSADMIN"`. Grants are issued as
`SECURITYADMIN`; for inherited grants, an explicit owner names the role holding
`MANAGE GRANTS` on the container and is used to issue the grant instead.

- **`inherited`** (`bool`, optional):
Turns a grant on all objects in a container into an inherited grant, which also covers
objects created later. Defaults to `false`.

**Note:** Inherited grants are a Snowflake preview feature, opted into with an account
parameter. Snowcap manages it with an [AccountParameter](account_parameter.md), applied
before any inherited grant that depends on it:

```yaml
account_parameters:
- name: FEATURE_RBAC_INHERITED_GRANTS
value: ENABLED
```

`snowcap plan` fails with a clear message if neither the account nor the config has opted
in. Snowflake does not allow inherited grants
to be combined with `WITH GRANT OPTION`, to carry `OWNERSHIP`, or to target shares and
integrations; `priv: ALL` is not supported either, so list privileges explicitly. See
[Managing access with inherited grants](https://docs.snowflake.com/en/user-guide/inherited-grants-intro).

**Note:** `IMPORTED PRIVILEGES` is only valid on a [SharedDatabase](shared_database.md)
(a database created `FROM SHARE`). It cannot be granted `WITH GRANT OPTION`
and can only be granted to account roles, not database roles. Snowflake's
`SHOW GRANTS` reports it as `USAGE` on shared databases — snowcap's fetch
logic handles this quirk transparently.

One `IMPORTED PRIVILEGES` grant also fans out in `SHOW GRANTS` into a row per object the
share exposes — every view, function, procedure, schema, database role, class, tag and
image repository in the database, which on the `SNOWFLAKE` database is several hundred
rows. Those rows are never in your config, so `--sync_resources grant` treats them as
covered by the declared grant rather than revoking them, the same way it treats the
per-object grants produced by an `ALL` or `INHERITED` grant.
Loading
Loading