Tundra
A fork of Permifrost for managing Snowflake permissions from a declarative YAML spec. It's faster on big accounts and understands the object types Snowflake has shipped since Permifrost stopped keeping up.
Anything not covered here (spec structure, read/write semantics, wildcards, ownership, --diff, --role,
--user, !include, require-owner) works exactly as it does upstream. See the
Permifrost README.
pip install tundra-cli
# or the latest main
pip install git+https://github.com/henryupton/tundra.git
tundra run spec.yml --dry
tundra spec-test spec.yml
Performance
Permifrost runs every Snowflake round trip serially, and on a large account most of them are wasted.
- Parallel fetch. Role grants, user grants and table-existence checks are fetched concurrently and merged
back in a deterministic order, so output is identical run to run. Concurrency is
--max-workers(orPERMISSION_BOT_MAX_WORKERS, default 8), and the connection pool is sized to match. - No enumeration under wildcards. A
db.schema.*grant is issued asON ALLplusON FUTURE, yet Permifrost still ranSHOW <objects> IN SCHEMAper schema, per object type, per role just to build its revoke allowlist. Tundra tracks wildcard coverage by schema scope instead, so those calls are gone. On one production spec that was 1,658s of a 4,938s apply. Emitted SQL is unchanged. - Skip what you don't need.
--skip-validationskips the entity-existence checks, and--ignore-missing-objectsdrops grants on missing objects rather than failing the run.
New object types
All table-like types ride the existing tables read/write lists, and a wildcard covers every type.
| Type | read | write | Schema write adds |
|---|---|---|---|
| Iceberg table | select |
insert, update, delete, truncate, references |
create iceberg table |
| Dynamic table | select |
monitor, operate |
|
| Streamlit | usage |
usage |
create streamlit |
| Semantic view | select |
select |
create semantic view |
These matter because Snowflake's ON ALL TABLES / ON ALL VIEWS don't reach any of them, so under Permifrost they
go silently ungranted. Schema write also grants create cortex search service and create mcp server. New types
are one entry in src/tundra/table_object_types.py.
External volumes are a top-level spec section, granted USAGE from a role's external_volumes: list (needed to
read or write Iceberg tables):
external_volumes:
- s3_iceberg:
storage_provider: s3
roles:
- data_engineer:
external_volumes: [s3_iceberg]
Database and application roles
A member_of entry containing a period is a DB.ROLE reference. Tundra checks it against
SHOW DATABASE ROLES, falls back to SHOW APPLICATION ROLES for an application of the same name, and grants or
revokes accordingly. Membership is fully managed: anything granted but not listed is revoked.
roles:
- monitor:
member_of:
- snowflake.usage_viewer # database role
- snowflake.cortex_analyst_requests_viewer # application role
Database and application roles are referenced, never defined, and must already exist. Spec roles can't contain a
period, and users can't be granted either kind. Grants an application made itself are never revoked. MANAGE GRANTS
doesn't reach the SNOWFLAKE application's roles, so the role Tundra runs as must hold every application role the
spec hands out.
Users
On top of Permifrost's user properties:
users:
- etl_service:
can_login: true
type: service # person (default), service, legacy_service
default_secondary_roles: [all]
parameters:
timezone: Pacific/Auckland # validated IANA name
week_start: 1
has_password: truerewritestype: servicetolegacy_service, since Snowflake won't let aSERVICEuser hold a password.has_password: falsesetsPASSWORD = NULL. Omit it forSERVICEusers.parameterssets user-level session defaults. Allowed keys aretimezone,week_start,week_of_year_policy, the*_output_formatfamily,timestamp_type_mapping,query_tag,statement_timeout_in_seconds,statement_queued_timeout_in_secondsandclient_session_keep_alive. Removing one leaves the value in place.- User identifiers and property values are quoted and uppercased properly.
Spec directories
run and spec-test accept a directory. Every .yml/.yaml under it (recursive, sorted, dotfiles skipped) is a
fragment holding any top-level keys, merged before validation:
spec/
settings.yml
databases.yml
roles/custom.yml
users/service.yml
List sections concatenate. An entity defined twice is an error naming both files. Scalar settings must agree.
Running it
Objects dropped mid-run. Grants are planned from a snapshot, so a schema that CI drops and recreates while
Tundra is applying would fail every queued statement. Tundra re-looks up the object instead: gone prints
[VANISHED] (not a failure), recreated is retried once. Every [ERROR] prints Snowflake's message beneath it.
Running role. The current role must be SECURITYADMIN or a role that has been granted it (checked with
IS_ROLE_IN_SESSION), so a purpose-built role works.
Workload identity federation. Short-lived OIDC tokens (GitHub Actions, say) with no stored secret:
PERMISSION_BOT_AUTHENTICATOR=WORKLOAD_IDENTITY
PERMISSION_BOT_WORKLOAD_IDENTITY_PROVIDER=OIDC
PERMISSION_BOT_TOKEN_COMMAND='python mint_token.py --audience my-account'
The command's stdout is the token, and it's rerun for every pooled connection so a long run never reuses an expired one. It takes precedence over the other auth methods, which are otherwise as upstream.
Warehouses. size is optional.
Contributing
Issues and PRs welcome on GitHub. Tests run on every PR. Versions are
bumped by the release workflow on merge to main, not by hand.
PYTHONPATH=src python -m pytest tests -q
Metadata
Release files for tundra-cli 1.0.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| tundra_cli-1.0.1.tar.gz | 95.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| tundra_cli-1.0.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 147.4 kB
Release files / tundra_cli-1.0.1.tar.gz
| Download URL | tundra_cli-1.0.1.tar.gz |
|---|---|
| Size | 95.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
8c84eb8cbb681a946286f843e0ddc898986fe2436c87e26fb0ea9ef0b2e46c8c
|
|
BLAKE2b-256 checksum How to use checksums |
ce18548ca35614defb7f61963b4a580c6ec1cc557809cde9b518c03f2d3ca5a2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 14, 2026.
Transparency logRelease files / tundra_cli-1.0.1-py3-none-any.whl
| Download URL | tundra_cli-1.0.1-py3-none-any.whl |
|---|---|
| Size | 51.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
9f1740089ec121a37c7cdcf6b81b2202496c5dc5a79714dcd3d8c4fdbe6e4027
|
|
BLAKE2b-256 checksum How to use checksums |
ce72c3675b9d1c119c449200af78495c6f788198a953a8444b9071342f7c28b8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 14, 2026.
Transparency log