Static RBAC Validation from Hasura Metadata: Build Your Own CLI Guardrail
Hasura's declarative RBAC is powerful — but dangerously quiet when misconfigured. You might think guest has no write access… until a policy-less table says otherwise. This guide walks you through building a CLI tool that parses Hasura metadata and verifies RBAC rules statically, before deployment. 1. Why Static Validation? Hasura permissions live in metadata/ as YAML or JSON. You can validate them without running the server — ideal for CI pipelines. Use cases: Detect tables with no select restrictions Ensure public role can’t write Enforce field-level allowlists Audit policy completeness per role 2. Directory Structure (exported via Hasura CLI) metadata/ ├── tables/ │ ├── user.yaml │ ├── invoice.yaml ├── actions/ ├── sources.yaml ├── version.yaml Each *.yaml under tables/ contains permissions by role: select_permissions: - role: user permission: columns: [id, email] filter: id: { _eq: X-Hasura-User-Id } 3. CLI Tool Overview We'll build a Node.js script that: Loads all tables/*.yaml Parses permissions per role Applies validation rules Outputs audit results or fails with nonzero exit code 4. Install Dependencies npm init -y npm install yaml fs glob chalk 5. Example CLI Logic import fs from 'fs'; import path from 'path'; import yaml from 'yaml'; import glob from 'glob'; import chalk from 'chalk'; const PERMIT_ROLES = ['admin', 'user']; const metadataPath = './metadata/tables'; const files = glob.sync(`${metadataPath}/*.yaml`); for (const file of files) { const content = fs.readFileSync(file, 'utf8'); const doc = yaml.parse(content); const table = `${doc.table.schema}.${doc.table.name}`; // Check for public access const publicInsert = doc.insert_permissions?.find(p => p.role === 'public'); if (publicInsert) { console.log(chalk.red(`❌ PUBLIC role has insert access on ${table}`)); process.exitCode = 1; } // Check for missing filters for (const role of PERMIT_ROLES) { const sel = doc.select_permissions?.find(p => p.role === role); if (sel && (!sel.permission.filter || Object.keys(sel.permission.filter).length === 0)) { console.log(chalk.yellow(`⚠️ Role ${role} has unfiltered SELECT on ${table}`)); } } } 6. Run It in CI "scripts": { "validate:rbac": "node rbac-check.js" } Then add to .github/workflows/ci.yml or GitLab CI pipeline: - name: Validate Hasura RBAC run: npm run validate:rbac 7. Extending the Ruleset

Hasura's declarative RBAC is powerful — but dangerously quiet when misconfigured.
You might think guest
has no write access… until a policy-less table says otherwise.
This guide walks you through building a CLI tool that parses Hasura metadata and verifies RBAC rules statically, before deployment.
1. Why Static Validation?
Hasura permissions live in metadata/
as YAML or JSON.
You can validate them without running the server — ideal for CI pipelines.
Use cases:
- Detect tables with no
select
restrictions - Ensure
public
role can’t write - Enforce field-level allowlists
- Audit policy completeness per role
2. Directory Structure (exported via Hasura CLI)
metadata/
├── tables/
│ ├── user.yaml
│ ├── invoice.yaml
├── actions/
├── sources.yaml
├── version.yaml
Each *.yaml
under tables/
contains permissions by role:
select_permissions:
- role: user
permission:
columns: [id, email]
filter:
id: { _eq: X-Hasura-User-Id }
3. CLI Tool Overview
We'll build a Node.js script that:
- Loads all
tables/*.yaml
- Parses permissions per role
- Applies validation rules
- Outputs audit results or fails with nonzero exit code
4. Install Dependencies
npm init -y
npm install yaml fs glob chalk
5. Example CLI Logic
import fs from 'fs';
import path from 'path';
import yaml from 'yaml';
import glob from 'glob';
import chalk from 'chalk';
const PERMIT_ROLES = ['admin', 'user'];
const metadataPath = './metadata/tables';
const files = glob.sync(`${metadataPath}/*.yaml`);
for (const file of files) {
const content = fs.readFileSync(file, 'utf8');
const doc = yaml.parse(content);
const table = `${doc.table.schema}.${doc.table.name}`;
// Check for public access
const publicInsert = doc.insert_permissions?.find(p => p.role === 'public');
if (publicInsert) {
console.log(chalk.red(`❌ PUBLIC role has insert access on ${table}`));
process.exitCode = 1;
}
// Check for missing filters
for (const role of PERMIT_ROLES) {
const sel = doc.select_permissions?.find(p => p.role === role);
if (sel && (!sel.permission.filter || Object.keys(sel.permission.filter).length === 0)) {
console.log(chalk.yellow(`⚠️ Role ${role} has unfiltered SELECT on ${table}`));
}
}
}
6. Run It in CI
"scripts": {
"validate:rbac": "node rbac-check.js"
}
Then add to .github/workflows/ci.yml
or GitLab CI pipeline:
- name: Validate Hasura RBAC
run: npm run validate:rbac