<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Georg</title>
    <description>The latest articles on DEV Community by Georg (@reachdiff).</description>
    <link>https://dev.to/reachdiff</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4169645%2F8dd3b843-d550-434d-98e0-e36ea95009dc.png</url>
      <title>DEV Community: Georg</title>
      <link>https://dev.to/reachdiff</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/reachdiff"/>
    <language>en</language>
    <item>
      <title>Who gains access when this Terraform plan is applied?</title>
      <dc:creator>Georg</dc:creator>
      <pubDate>Wed, 07 Oct 2026 19:13:26 +0000</pubDate>
      <link>https://dev.to/reachdiff/who-gains-access-when-this-terraform-plan-is-applied-237c</link>
      <guid>https://dev.to/reachdiff/who-gains-access-when-this-terraform-plan-is-applied-237c</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0a45uy3pw1wrvg0ora0d.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0a45uy3pw1wrvg0ora0d.gif" alt="reachdiff live run: BLOCK, gp-interns gains READ on the hr schema" width="798" height="193"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A pull request that changes Unity Catalog grants usually looks harmless. One line, one group, one privilege:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;grant&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;principal&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"gp-interns"&lt;/span&gt;
  &lt;span class="nx"&gt;privileges&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"SELECT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"USE_SCHEMA"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;terraform plan&lt;/code&gt; tells you that &lt;code&gt;databricks_grants.hr&lt;/code&gt; will be updated in place. It does not tell you who can read what afterwards. That depends on things the diff doesn't show: who is in the group (and in the groups inside it), which usage privileges already exist further up, who owns what, and which other resources touch the same objects.&lt;/p&gt;

&lt;p&gt;I wanted the review question answered directly: &lt;strong&gt;who gains or loses access to what when this plan is applied?&lt;/strong&gt; So I built &lt;strong&gt;reachdiff&lt;/strong&gt;, a small open-source CLI. It reads &lt;code&gt;terraform show -json&lt;/code&gt;, works out the effective-access diff, shows the route behind each change and returns an exit code a pipeline can act on.&lt;/p&gt;
&lt;h2&gt;
  
  
  What it reports
&lt;/h2&gt;

&lt;p&gt;For the change above, run against a test workspace:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ reachdiff plan --tfplan plan.json --profile PROFILE
reachdiff: BLOCK (live mode)
1 gained, 0 lost, 4 findings

+ gp-interns (1 member, 1 new, 0 already had it)  READ  main.hr (all tables)
    via SELECT on main.hr + USE_CATALOG on main + USE_SCHEMA on main.hr

Findings:
  BLOCK grant_resource_conflict main.hr.salaries: databricks_grants and databricks_grant both manage it; the applied
        result depends on apply order, and grants can be lost without an error
  WARN sensitive_access gp-interns READ main.hr: gp-interns gains READ on sensitive data in main.hr
  WARN unverified latent_grants: gp-interns gains USE_SCHEMA on main.hr; this may activate existing table-level grants
        that were not enumerated (use --deep)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Four things happen here that the plan diff doesn't show:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The gain is expressed as access, not as privileges.&lt;/strong&gt; &lt;code&gt;READ&lt;/code&gt; on the whole schema, with the route: &lt;code&gt;SELECT&lt;/code&gt; on the schema, &lt;code&gt;USE_CATALOG&lt;/code&gt; inherited through another group, &lt;code&gt;USE_SCHEMA&lt;/code&gt; from this change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The group is expanded.&lt;/strong&gt; One member, and that member didn't have this access before.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sensitive data is flagged.&lt;/strong&gt; The schema contains a table tagged &lt;code&gt;sensitive&lt;/code&gt;; the default patterns are &lt;code&gt;pii*&lt;/code&gt;, &lt;code&gt;sensitive*&lt;/code&gt; and &lt;code&gt;class.*&lt;/code&gt; (the tags Databricks data classification writes).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A second resource on the same table blocks the run.&lt;/strong&gt; More on that below, because it was the most surprising thing I saw.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Exit codes: &lt;code&gt;0&lt;/code&gt; below the threshold, &lt;code&gt;1&lt;/code&gt; WARN, &lt;code&gt;2&lt;/code&gt; BLOCK, &lt;code&gt;3&lt;/code&gt; invalid input or an operational failure. The threshold defaults to BLOCK and is configurable, as are your own rules in TOML (for example: new access to PII only through approved groups).&lt;/p&gt;
&lt;h2&gt;
  
  
  Three things a real workspace taught me
&lt;/h2&gt;

&lt;p&gt;I wrote the first version against synthetic plans and documented API shapes. Then I ran a list of predictions against an Azure Databricks trial workspace with synthetic groups, users and tables, and recorded what actually happened. Three results changed the tool.&lt;/p&gt;
&lt;h3&gt;
  
  
  1. Two grant resources on one object can silently remove grants
&lt;/h3&gt;

&lt;p&gt;The Databricks Terraform provider has an authoritative resource (&lt;code&gt;databricks_grants&lt;/code&gt;: "these are all the grants on this object") and an additive one (&lt;code&gt;databricks_grant&lt;/code&gt;: "this principal has these privileges"). If both manage the same securable, the result depends on apply order. In the test, I moved a table's grants from &lt;code&gt;databricks_grants&lt;/code&gt; to &lt;code&gt;databricks_grant&lt;/code&gt;: one destroy and one create in the same plan. Terraform ran them in parallel, the create finished first, and the destroy then removed every grant on the table. Apply reported success. Only the next plan noticed that a grant was missing.&lt;/p&gt;

&lt;p&gt;reachdiff now treats more than one resource setting grants on one securable as &lt;code&gt;grant_resource_conflict&lt;/code&gt;, BLOCK by default when the plan changes access. If you know the IAM policy versus binding versus member resources from other providers, it's the same class of problem.&lt;/p&gt;
&lt;h3&gt;
  
  
  2. A usage privilege can switch on grants nobody is looking at
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;SELECT&lt;/code&gt; on a table does nothing without &lt;code&gt;USE_SCHEMA&lt;/code&gt; and &lt;code&gt;USE_CATALOG&lt;/code&gt;. So a change that only adds &lt;code&gt;USE_SCHEMA&lt;/code&gt; can activate table-level grants that have been sitting there unused, possibly for years. The plan shows one usage privilege. reachdiff reports a &lt;code&gt;latent_grants&lt;/code&gt; gap by default, and with &lt;code&gt;--deep&lt;/code&gt; it reads the child tables and turns those activations into actual gains.&lt;/p&gt;
&lt;h3&gt;
  
  
  3. The scanner sees less than you think, quietly
&lt;/h3&gt;

&lt;p&gt;Two findings about the identity that runs the check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Without &lt;code&gt;READ METADATA&lt;/code&gt; on the catalog (or the metastore), the permissions API returns only the caller's own grants. That isn't an error, it's just an incomplete answer. reachdiff checks its own visibility and discards grant lists it can't trust, so such a run can't pass.&lt;/li&gt;
&lt;li&gt;Workspace SCIM shows group members only to workspace admins. Without admin rights, reachdiff says "members not read" and raises a &lt;code&gt;membership_incomplete&lt;/code&gt; gap instead of pretending a group is empty.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's the general design: &lt;strong&gt;anything reachdiff can't see becomes a finding&lt;/strong&gt;. The &lt;code&gt;unverified&lt;/code&gt; rule can be raised to BLOCK but never turned off.&lt;/p&gt;
&lt;h2&gt;
  
  
  In CI
&lt;/h2&gt;

&lt;p&gt;There's a GitHub Action and an Azure DevOps template. Both log in to Databricks by token federation (&lt;code&gt;github-oidc&lt;/code&gt;, &lt;code&gt;azure-devops-oidc&lt;/code&gt;), so no Databricks secret is stored in CI. The report goes into one pull request comment (a thread on Azure DevOps), which is updated in place on every push, and the job result follows the status: WARN, BLOCK or error.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2qbgl1ryi91ptjedspyz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2qbgl1ryi91ptjedspyz.png" alt="The reachdiff comment on a GitHub pull request, edited to BLOCK after the second commit" width="800" height="662"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F56lrlrhz4sr6m0cbljdn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F56lrlrhz4sr6m0cbljdn.png" alt="Azure DevOps pull request: required reachdiff check failed" width="800" height="378"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Reports contain principal and object names, so the publish step refuses public repositories unless you opt in.&lt;/p&gt;
&lt;h2&gt;
  
  
  What it doesn't do
&lt;/h2&gt;

&lt;p&gt;I'd rather say this up front than in the comments:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Validated scope:&lt;/strong&gt; live mode was exercised against one Azure Databricks workspace (Premium, classic compute), with the provider version and SDK version listed in the README, and in CI on GitHub-hosted Ubuntu and a self-hosted Azure DevOps agent. AWS, GCP, serverless workspaces and multiple workspaces are not covered yet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not evaluated at all:&lt;/strong&gt; ABAC policies, row filters and column masks; volumes, functions and external locations; workspace-level ACLs; metastore and workspace admin powers; runtime behavior.&lt;/li&gt;
&lt;li&gt;It's a review aid for plans, not an audit of the current state of your metastore.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s1"&gt;'reachdiff[databricks]'&lt;/span&gt;
terraform show &lt;span class="nt"&gt;-json&lt;/span&gt; plan.bin &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; plan.json
reachdiff plan &lt;span class="nt"&gt;--tfplan&lt;/span&gt; plan.json &lt;span class="nt"&gt;--profile&lt;/span&gt; PROFILE &lt;span class="nt"&gt;--format&lt;/span&gt; md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;There's also an offline mode with no dependencies and a synthetic example plan in the repository, so you can see the report without a workspace.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/reachdiff" rel="noopener noreferrer"&gt;
        reachdiff
      &lt;/a&gt; / &lt;a href="https://github.com/reachdiff/reachdiff" rel="noopener noreferrer"&gt;
        reachdiff
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Who gains or loses Databricks Unity Catalog access when a Terraform plan is applied
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;reachdiff&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Who gains or loses access to what when this Terraform plan is applied?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;reachdiff reads &lt;code&gt;terraform show -json&lt;/code&gt; output for Databricks Unity Catalog grants
(&lt;code&gt;databricks_grants&lt;/code&gt;, &lt;code&gt;databricks_grant&lt;/code&gt;), group memberships (&lt;code&gt;databricks_group_member&lt;/code&gt;)
and owners. It reports the &lt;strong&gt;effective-access diff&lt;/strong&gt;, with the route behind each change, and
returns an exit code a pipeline can act on.&lt;/p&gt;
&lt;p&gt;Status: beta. Live mode has been exercised against one Azure Databricks workspace; see
&lt;a href="https://github.com/reachdiff/reachdiff#validated-scope" rel="noopener noreferrer"&gt;Validated scope&lt;/a&gt; for what that covers and what it doesn't. The demo and tests use synthetic plans.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Install&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;Python 3.11 or newer:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;pip install reachdiff                  &lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; offline mode, no dependencies&lt;/span&gt;
pip install &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;reachdiff[databricks]&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;    &lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; live mode, adds databricks-sdk&lt;/span&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Try the synthetic demo&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;Python 3.11 or newer, no dependencies:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;python3 -m reachdiff plan --tfplan examples/schema-grant.plan.json --offline&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;It reports &lt;code&gt;analysts&lt;/code&gt; gaining READ on all of &lt;code&gt;prod.sales&lt;/code&gt; (3 members: 2 new, 1 already had it)
and &lt;code&gt;bob@example.com&lt;/code&gt; losing WRITE…&lt;/p&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/reachdiff/reachdiff" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt;Code and docs: &lt;a href="https://github.com/reachdiff/reachdiff" rel="noopener noreferrer"&gt;https://github.com/reachdiff/reachdiff&lt;/a&gt; (Apache-2.0)&lt;/li&gt;
&lt;li&gt;Package: &lt;a href="https://pypi.org/project/reachdiff/" rel="noopener noreferrer"&gt;https://pypi.org/project/reachdiff/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GitHub Action: &lt;a href="https://github.com/marketplace/actions/reachdiff" rel="noopener noreferrer"&gt;https://github.com/marketplace/actions/reachdiff&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's version 0.9.0, a beta. I'm especially interested in runs on AWS or GCP, in plans that confuse it, and in how you review grant changes today.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;reachdiff is an independent side project, not affiliated with or endorsed by Databricks or HashiCorp. I built it with Claude Code as a pair programmer; the behavior described here was checked against a real workspace, and what wasn't is listed in the README as not covered.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>databricks</category>
      <category>terraform</category>
      <category>devops</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
