You push to staging. It works.
You deploy to prod. And suddenly, users can write to audit_logs
.
The problem?
RBAC drift between environments — subtle, silent, and security-critical.
This guide walks through detecting RBAC differences between dev and prod Hasura metadata by comparing generated Role × Field matrices.
1. Assumptions
- You export Hasura metadata from both environments:
hasura metadata export --endpoint https://dev-api.example.com --output metadata-dev/
hasura metadata export --endpoint https://prod-api.example.com --output metadata-prod/
- You have a matrix generator like
rbac-matrix.js
(see previous article) that produces:rbac-dev.csv
rbac-prod.csv
2. Diff Goals
We want to detect:
✅ Added/removed access
✅ Role gaining new permissions
✅ Field-level permission divergence
✅ Action type mismatches (e.g. SELECT
allowed in dev, denied in prod)
3. Matrix Normalization
Ensure both rbac-dev.csv
and rbac-prod.csv
are:
- Sorted by
Role, Table, Field
- Have the same headers:
Role,Table,Field,SELECT,INSERT,UPDATE,DELETE
4. Diff Script (TypeScript / Node.js)
import fs from 'fs';
import { parse } from 'csv-parse/sync';
const dev = parse(fs.readFileSync('rbac-dev.csv'), { columns: true });
const prod = parse(fs.readFileSync('rbac-prod.csv'), { columns: true });
const key = (row: any) => `${row.Role}::${row.Table}::${row.Field}`;
const mapDev = new Map(dev.map(row => [key(row), row]));
const mapProd = new Map(prod.map(row => [key(row), row]));
const allKeys = new Set([...mapDev.keys(), ...mapProd.keys()]);
for (const k of allKeys) {
const d = mapDev.get(k);
const p = mapProd.get(k);
if (!d) {
console.log(`🟢 Added in PROD: ${k}`);
continue;
}
if (!p) {
console.log(`🔴 Missing in PROD: ${k}`);
continue;
}
['SELECT', 'INSERT', 'UPDATE', 'DELETE'].forEach(op => {
if ((d[op] || '') !== (p[op] || '')) {
console.log(`⚠️ Diff in ${k} → ${op}: DEV=${d[op]} PROD=${p[op]}`);
}
});
}
5. CI Integration
- name: Generate RBAC Matrices
run: |
node rbac-matrix.js metadata-dev/ > rbac-dev.csv
node rbac-matrix.js metadata-prod/ > rbac-prod.csv
- name: Detect RBAC Drift
run: node diff-rbac.js
Use exitCode = 1
if drift is critical.
6. Optional Enhancements
- Export to Markdown table for PR comments
- Group diffs by role or table
- Visual HTML diff viewer with color-coded deltas
- Auto-approve deploy only if diff is "approved"
Final Thoughts
Your app changes often.
Your schema changes faster.
Your permissions? They must stay in sync — or you're inviting trouble.
With matrix diffing in place, RBAC drift becomes an explicit, testable signal — not an incident waiting to happen.
Next:
- PR Bot to annotate diffs
- Approval workflow with policy-as-code
- Field-level explainers for detected mismatches
Track the access. Visualize the change. Secure the drift.