What you will be able to do
- Configure grants on models, seeds and snapshots in dbt_project.yml, YAML property files and config() blocks
- Predict which grant and revoke statements dbt runs when a resource builds
- Choose between clobbering and adding grantees with the + privilege prefix
- Tell apart revoking every grant from no longer having dbt manage grants
- Adapt privilege and grantee names to BigQuery, Redshift, Databricks and Snowflake, and know when to use hooks instead
Key concept
Grants as declared state — The grants config states which recipients should hold which privileges on a model, seed or snapshot. Each time that resource builds, dbt runs whatever grant and revoke statements are needed so the database matches that state exactly.
1.What the grants config controls and where it is set
Database permissions decide who can read, insert into or drop a table or view. A finished mart is only useful once the BI tool's account is allowed to select from it. The grants config brings those permissions into the dbt project, so dbt applies them at build time, as part of creating the object, instead of leaving them as a separate admin task after the run.
A grant has two parts. The privilege is the action being allowed, such as select. The grantees are the recipients. Depending on the platform, a grantee can be a user, a group, a Snowflake role or a GCP service account. In the config, each privilege is a key and its grantees are the value. The value can be a single name or a list.
models:
- name: specific_model
config:
grants:
select: ['reporter', 'bi']Grants work on three resource types: models, seeds and snapshots. Each type can be configured in three places, and the layers work together. Defaults that cover the whole project, a package or a subfolder go in dbt_project.yml. Resource-specific grants go in a YAML config: block, as above, or in a {{ config() }} call inside the .sql file. Snapshots follow the same pattern, using a snapshots property file or the snapshot's own config(). When more than one layer applies, the usual [config precedence](lesson:1.6) decides, and the most specific setting wins. Grants are also recorded in the project's metadata, including the manifest artifact.
Checkpoint 1 of 8· Check yourself
A team wants every model in the project to be readable by the reporter role, and one seed also readable by bi. Which setup does the grants config support?
Grants can be set on models, seeds and snapshots. dbt_project.yml holds the project defaults, and each resource can define its own grants in YAML or SQL.
“define grants as resource configs on each model, seed, or snapshot”Source: docs.getdbt.com
2.What dbt runs when a resource builds
On the first build, dbt creates the object and then grants. Take an incremental model that grants select to two recipients:
{{ config(materialized = 'incremental', grants = {
'select': ['bi_user', 'reporter']
}) }}The first run creates the table and then issues something like grant select on schema_name.incremental_model to bi_user, reporter;. Later runs behave differently. The table already exists, so dbt first runs database-specific SQL to list the grants it already has. It then compares that list with the config and sends only the grant or revoke statements that close the gap. If someone grants select to an extra user by hand between runs, that user is not in the config, so the next build revokes the grant.
The exact statements depend on the adapter and on whether dbt is replacing the object or updating one that already exists. To see what actually ran, check the debug logs. They record every grant and revoke statement dbt issued.
Checkpoint 2 of 8· Put it in order
Put these steps in the order they happen for the incremental model above, across its first and second runs
- 1.On the next run, dbt uses database-specific SQL to show the grants already on incremental_model
- 2.dbt creates the incremental_model table
- 3.dbt runs a statement like grant select on schema_name.incremental_model to bi_user, reporter
- 4.dbt issues only the grant or revoke statements needed to match the configured grants
On the first run, dbt grants right after creating the object. On later runs it reads the current grants first and then sends only the statements needed to reach the configured state.
“dbt will use database-specific SQL to show the grants already on incremental_model, and then determine if any revoke or grant statements are needed”Source: docs.getdbt.com
Sources1
3.Clobbering versus adding grantees with the + prefix
models:
+grants: # In this case the + is not optional, you must include it for your project to parse.
select: ['user_a', 'user_b']By default, a more specific grant list replaces a less specific one. dbt calls this "merge and clobber", and it settles the merge at parse time, one privilege at a time. With the project file above and a model config() that grants select to ['user_c'], the database ends up with exactly one grantee, user_c, and the logs show exactly one grant statement. An analyst who adds one person to a single model can therefore remove everyone the project default had granted.
To add to the inherited list instead, prefix the privilege name with +. Writing {{ config(grants = {'+select': ['user_c']}) }} grants select to user_a, user_b and user_c. The prefix works per privilege. If the same config also grants insert without a +, the insert list still clobbers. grants is currently the only config that supports this merge-control +.
Grant values can also use Jinja, like any other config. A common pattern switches grantees on target.name, so production and development builds grant to different people:
models:
+grants:
select: "{{ ['user_a', 'user_b'] if target.name == 'prod' else ['user_c'] }}"Checkpoint 3 of 8· Fill the gap
The project grants select to user_a and user_b. Which privilege key makes this model config keep both of them and also grant to user_c?
{{ config(grants = {' ? ': ['user_c']}) }}Prefixing the privilege with + switches it from clobbering to adding, so user_c joins user_a and user_b. A plain select key would leave user_c as the only grantee.
Source: docs.getdbt.comCheckpoint 4 of 8· Exam question
A dbt project sets a project-level default in `dbt_project.yml`: ```yaml models: +grants: select: ['analyst_role', 'bi_role'] ``` A specific model's SQL file then adds: ```sql {{ config(grants = {'select': ['reporting_role']}) }} ``` After the next run, which grantees actually hold the `select` privilege on that model?
Correct answer: A — Only `reporting_role` holds `select`, because a model-level grants config replaces the project-level default for that privilege rather than merging with it.
- A. The default grants behavior is merge-and-clobber: a more specific config for the same privilege replaces the less specific one instead of combining with it. Since the model's inline config redefines `select`, it overrides the project-level list entirely.
- B. dbt does not union grantee lists across config levels by default; the plain privilege key always replaces rather than merges. Automatic unioning only happens when the privilege key is prefixed with `+`, which this example does not use.
- C. Project-level defaults in `dbt_project.yml` are the least specific config location, so a model's own config block always wins for that privilege when both define it. This is the reverse of what actually happens here.
- D. dbt does not skip a privilege just because it is set in more than one place; it resolves the conflict deterministically by specificity. The model still ends up with a concrete grantee list after the run.
4.Revoking grants versus no longer managing them
dbt only touches grants on a node, including revoking them, when that node has a grants config attached. This rule decides what happens when someone edits or deletes a grants section. Start from a project that grants select to ['user_a', 'user_b'] and consider three edits.
| Edit in dbt_project.yml | What dbt does on the next build |
|---|---|
| select: ['user_b'] | Revokes select from user_a. user_b keeps it |
| select: [] | Revokes select from every grantee |
| Delete the whole +grants section | Stops managing grants and changes nothing, so user_a and user_b keep select |
The third row catches people. Deleting the config looks like "remove the access", but dbt reads it as "stop managing access", so existing grants stay on the table. To lock a model down, keep the privilege key and give it an empty list.
Checkpoint 5 of 8· Check yourself
A model containing PII must stop being readable by the analysts who currently hold select through the project's grants config. Which change makes dbt remove that access on the next build?
An empty grantee list tells dbt to revoke every existing grant. Removing or commenting out the config only stops dbt from managing grants, so the existing ones stay.
“To have dbt revoke all existing grants from a node, provide an empty list of grantees.”Source: docs.getdbt.com
Checkpoint 6 of 8· Exam question
The project's `dbt_project.yml` grants `select` to `finance_reporting` at the project level. One model needs an additional grantee, `finance_audit`, without losing `finance_reporting`, and without changing the merge-and-clobber default anywhere else in the project. Which config change on that model achieves this?
Correct answer: A — Add `{{ config(grants = {'+select': ['finance_audit']}) }}` to the model, which appends the new grantee to the inherited `select` list instead of replacing it.
- A. Prefixing a privilege key with `+` tells dbt to add the listed grantees to whatever the parent config already defines for that privilege, rather than clobbering it. This is exactly the additive behavior needed here.
- B. A plain `select` key without the `+` prefix always replaces the inherited grantee list for that privilege; it does not merge automatically. Using this form would drop `finance_reporting` from the model.
- C. Moving the config to the properties file still follows plain merge-and-clobber rules for that single model; it does not change how any other model in the project resolves grants. It also does not use the additive mechanism the scenario asks for.
- D. The `grants` config block does support additive behavior through the `+` prefix, so a raw SQL hook is unnecessary here. Reaching for a hook adds an unmanaged side effect that dbt's built-in grants tracking will not account for.
Sources1
5.Platform-specific names, and when hooks take over
select and a plain user name are common conventions, but every platform names privileges and grantees in its own way, and dbt passes your names through as written. On BigQuery, privileges are IAM roles, such as roles/bigquery.dataViewer. Grantees carry a type prefix, so a person is written as user:someone@yourcompany.com.
{{ config(grants = {'roles/bigquery.dataViewer': ['user:someone@yourcompany.com']}) }}| Platform | What to know |
|---|---|
| BigQuery | Privileges are roles like roles/bigquery.dataViewer. Grantees need a prefix such as user:. grants is separate from grant_access_to, which sets up authorized views |
| Redshift | Prefix group: or role: for groups and roles. Unprefixed names are users |
| Databricks | Grants are enabled automatically on SQL endpoints. Interactive clusters need table access control enabled. READ_METADATA and USAGE need post-hooks |
| Snowflake | copy_grants is taken into account. Only Snowflake roles, not database roles, are fully supported |
| OSS Apache Spark / Delta Lake | Grants are not supported |
models:
+grants:
select: ["user1", "user:user2", "group:analysts", "role:reporter"]A hook is a SQL statement attached to a resource through its pre_hook or post_hook config, which dbt runs before or after that resource builds. A hand-written grant statement in a hook is the alternative to the grants config. Use resource configs for grants wherever possible, and switch to hooks only when the config cannot do the job. The cases that call for hooks are:
- grants on objects other than tables and views - row- or column-level access, masking policies, or future grants - platform permission features dbt has no config for, or grant logic more complex than the built-in config allows
Model access is a separate setting. access: public controls which other projects may ref a model. It grants nothing in the warehouse. A public model still needs a grants config, or some other permission mechanism, before anyone outside dbt can query it.
Checkpoint 7 of 8· Match them up
Match each platform to how grants behave there
Tap a term, then the definition that fits it.
Each adapter has its own names for privileges and grantees, and some privileges or platforms are outside what the grants config can handle.
“Use the group: or role: prefix in grantee names to grant to groups or roles. Unprefixed names are treated as users.”Source: docs.getdbt.com
Checkpoint 8 of 8· Exam question
A team migrating a Snowflake-based dbt project to BigQuery reuses their old `grants` config and finds it rejected at parse time. Which changes are required to make the config valid for the BigQuery adapter? (Select all that apply)(Select 2)
Correct answers: A, B — Rewrite the privilege keys as BigQuery IAM role names prefixed with `roles/`, such as `roles/bigquery.dataViewer`, instead of a generic keyword like `select`.; Prefix each grantee identifier with its principal type, for example `user:analyst@company.com` or `group:reporting@company.com`, instead of a bare email address.
- A. BigQuery has no `select`/`insert` keyword privileges; instead the privilege key must be a BigQuery IAM role name such as `roles/bigquery.dataViewer`. Reusing Snowflake-style keywords will fail to parse on this adapter.
- B. BigQuery grantees must carry a principal-type prefix like `user:`, `group:`, `serviceAccount:`, or `domain:` so the platform knows what kind of identity is receiving the role. A bare email address alone is not a valid grantee on this adapter.
- C. The `grants` config and `grant_access_to` are two different mechanisms; the latter configures BigQuery authorized views and does not replace ordinary grants. BigQuery does support the standard `grants` config alongside `grant_access_to`.
- D. Quote style in the YAML or Jinja dictionary has no bearing on whether BigQuery accepts the config; both quoting styles parse identically. The actual failure comes from using the wrong privilege and grantee syntax, not the quote character.
- E. Grants configs live in `dbt_project.yml`, model config blocks, or properties files regardless of adapter; `packages.yml` is only for declaring package dependencies. Relocating the config there would not be read by dbt at all.
Exam traps
Each one states something that sounds right. Open it to see what is actually true.
1.Deleting a model's grants config makes dbt revoke the grants it had applied.Why is that wrong?
When there is no grants config, dbt stops managing grants and leaves the existing ones in place. Only an empty grantee list revokes them.
2.A model-level grants list adds its grantees to the ones set in dbt_project.yml.Why is that wrong?
By default, the more specific list replaces the less specific one for that privilege. Grantees are added only when the privilege name has a + prefix, such as '+select'.
Covered in Clobbering versus adding grantees with the + prefix
3.The + in '+select' inside a model's config() is the same + used before config keys in dbt_project.yml.Why is that wrong?
In dbt_project.yml, + marks a config key. In a privilege name, + switches merging from clobber to add. They are two different features.
Covered in Clobbering versus adding grantees with the + prefix
4.Setting access: public on a model grants select on it to warehouse users.Why is that wrong?
Model access only governs ref from other projects. Database permissions still come from grants or another mechanism.
Covered in Platform-specific names, and when hooks take over
5.The grants config can set future grants and column-level access.Why is that wrong?
Future grants, row- and column-level access and masking policies are outside the built-in config, so they need grant statements run through hooks.
Covered in Platform-specific names, and when hooks take over
Sources
Every claim above is drawn from one of these pages, quoted as it was written on the date shown.
- 1.
“define grants as resource configs on each model, seed, or snapshot”
↩︎ What the grants config controls and where it is set“Privilege: A right to perform a specific action or set of actions on an object in the database”
↩︎ What the grants config controls and where it is set“Like all configurations, grants will be included in dbt project metadata, including the manifest artifact.”
↩︎ What the grants config controls and where it is set“When dbt runs this model for the first time, it will create the table, and then run code like:”
↩︎ What dbt runs when a resource builds“You can always check the debug logs for the full set of grant and revoke statements that dbt runs.”
↩︎ What dbt runs when a resource builds“dbt's default behavior replaces the less-specific set of grantees with the more-specific set of grantees”
↩︎ Clobbering versus adding grantees with the + prefix“Like any other config, you can use Jinja to vary the grants in different contexts.”
↩︎ Clobbering versus adding grantees with the + prefix“dbt only modifies grants on a node (including revocation) when a grants configuration is attached to that node.”
↩︎ Revoking grants versus no longer managing them“To have dbt revoke all existing grants from a node, provide an empty list of grantees.”
↩︎ Revoking grants versus no longer managing them“On BigQuery, "privileges" are called "roles," and they take the form roles/service.roleName.”
↩︎ Platform-specific names, and when hooks take over“OSS Apache Spark / Delta Lake do not support grants.”
↩︎ Platform-specific names, and when hooks take over“Granting to / revoking from is only fully supported for Snowflake roles (not database roles).”
↩︎ Platform-specific names, and when hooks take over“You should define grants as resource configs whenever possible, but you might occasionally need to write grants statements manually and run them using hooks.”
↩︎ Platform-specific names, and when hooks take over“dbt ensures that the grants on its view or table match exactly the grants you have configured”
↩︎ Key concept“If you delete the entire +grants section, dbt assumes you no longer want it to manage grants and doesn't change anything.”
↩︎ Exam trap 1“This will only take effect for privileges which include the + prefix.”
↩︎ Exam trap 2“This use of +, controlling clobber vs. add merge behavior, is distinct from the use of + in dbt_project.yml”
↩︎ Exam trap 3“Create more granular row- and column-level access, use masking policies, or apply future grants.”
↩︎ Exam trap 5“dbt will use database-specific SQL to show the grants already on incremental_model, and then determine if any revoke or grant statements are needed”
↩︎ Checkpoint“Use the group: or role: prefix in grantee names to grant to groups or roles. Unprefixed names are treated as users.”
↩︎ Checkpoint - 2.https://docs.getdbt.com/reference/model-configsOfficial docs
“The most specific configuration always takes precedence.”
↩︎ What the grants config controls and where it is set“post_hook: <sql-statement> | [<sql-statement>]”
↩︎ Platform-specific names, and when hooks take over - 3.
“They ensure that only authorized users can perform specific actions.”
↩︎ What the grants config controls and where it is set - 4.
“Currently, the only config which supports + for controlling config merge behavior is grants.”
↩︎ Clobbering versus adding grantees with the + prefix - 5.
“does not trigger dbt to automagically grant select on that model to every user or role in your data platform”
↩︎ Platform-specific names, and when hooks take over“does not trigger dbt to automagically grant select on that model to every user or role in your data platform”
↩︎ Exam trap 4