Skip to content

Repository files navigation

GitOps RBAC Provisioner

Declarative access control for data warehouses. Define roles and grants as YAML in a config directory of your choosing — policies/ by convention, but any path you point the tool at — and let the tool reconcile your database to match — the same plan/apply model as Terraform, but for GRANT/REVOKE.

Instead of access changes living in someone's memory, a stale Slack thread, or a one-off GRANT run manually during an incident and never cleaned up, this tool makes access control auditable, version-controlled, and enforced by CI instead of by tribal knowledge.

Why this exists

Database permissions drift silently. An on-call engineer runs a manual GRANT to unblock an incident, nobody remembers to revoke it, and months later a security audit stumbles onto access nobody can explain. This tool closes that loop: every access change goes through a reviewed pull request, plan shows exactly what will change before it happens, and a scheduled drift check catches anything that slips outside that process.

How it works

<config-dir>/*.yml (desired state)   ← any folder, passed via --config
            vs
  live database grants (actual state)
            ↓
    diff_engine computes the delta
            ↓
  plan  → show the diff, change nothing
  apply → execute GRANT/REVOKE to close the gap

Screenshots

plan — previews the diff, changes nothing:

gitops-rbac-provisioner plan output

apply — executes the reconciliation:

gitops-rbac-provisioner apply output

Features

  • Single declarative policy file. All roles, grants, denies, and PII tags live in one reviewable config file, inside whatever directory you point --config at (policies/ by default).
  • Role inheritance. Roles can inherit from other roles via member_of, with effective grants resolved recursively (and guarded against circular inheritance).
  • Deny always wins. Explicit deny rules override any broader grant, enforced at diff time — not just declared as intent.
  • Column-level tagging. Sensitive columns (e.g. PII) are tagged once in tags: and referenced by name from any role, rather than repeated everywhere.
  • Safe by default. Every run is a dry-run unless --apply is explicitly passed. Destructive changes touching a role's own schema are blocked outright; revokes touching an active database session require an explicit --force.
  • Wildcard expansion. tables: all grants are resolved against the live schema at diff time, so newly added tables are picked up automatically.
  • Unmanaged-role protection. The tool never touches roles it doesn't manage — only roles declared in your config directory are ever granted or revoked.
  • No fixed config location. --config accepts any directory or single file on disk. This repo happens to keep its own policies in policies/, but that's just a convention, not a requirement — e.g. --config teams/data-eng/rbac/ works exactly the same way.
  • CI-native. Every plan and apply runs through GitHub Actions, with the diff posted directly as a PR comment for review.
  • Reusable workflows. plan, apply, and drift_check are built as reusable GitHub Actions workflows (workflow_call), so any repo can invoke them with a thin wrapper instead of copy-pasting CI logic.
  • Scheduled drift detection. A recurring job independently checks the live database against policy, catching manual out-of-band changes and alerting to Slack or GitHub Issues based on severity.
  • Pluggable adapter interface. Built against PostgreSQL first; the adapter interface (DBAdapter) is designed so other warehouses (Snowflake, BigQuery) can be added without touching the diff engine.

Quickstart

# Install dependencies
cp .env.example .env
pip install -r requirements.txt

# Preview what would change (read-only, safe to run anytime)
python -m provisioner.cli plan --config policies/ --conn-string "$DATABASE_URL"

# Apply the changes
python -m provisioner.cli apply --config policies/ --conn-string "$DATABASE_URL" --apply

policies/ above is just this repo's own folder for its policy YAML — the --config flag isn't tied to that name. Point it at whatever directory (or single file) you keep your roles/grants in:

python -m provisioner.cli plan --config path/to/your-folder/ --conn-string "$DATABASE_URL"
python -m provisioner.cli apply --config path/to/your-folder/ --conn-string "$DATABASE_URL" --apply

Project structure

gitops-rbac-provisioner/
├── .github/
│   └── workflows/
│       ├── plan.yml            # reusable workflow: diff + PR comment
│       ├── apply.yml           # reusable workflow: reconcile on merge to main
│       └── drift_check.yml     # reusable workflow: scheduled drift detection
├── config/
│   └── guardrail.yml           # Basic config for tool
├── provisioner/
│   ├── cli.py                  # entrypoint: plan / apply / check-drift
│   ├── state_loader.py         # parses & validates config.yml into desired state
│   ├── diff_engine.py          # core reconciliation logic
│   ├── sql_generator.py        # diff -> GRANT/REVOKE SQL
│   ├── executor.py             # dry-run vs real execution, commit/rollback
│   ├── pr_commenter.py         # posts the plan diff to the PR
│   ├── notifier.py             # Slack + GitHub issue alerts for drift
│   └── adapters/
│       ├── base.py             # abstract DBAdapter interface
│       └── postgres_adapter.py # concrete Postgres implementation
├── policies/                    # default --config path; point elsewhere if you prefer
│   └── policy.yml              # roles, grants, denies, and PII tags
├── tests/
├── .env.example                 # sample DB connection / credential vars
├── .gitignore                   # ignores .env, __pycache__, .venv, etc.
├── README.md                    # setup, usage, and architecture overview
├── requirements.txt
├── seed.sql                     # bootstrap schema/roles for local/dev testing
└── pyproject.toml

Policy file example

The file below can live anywhere — this repo defaults to policies/policy.yml, but any path passed to --config works the same way.

version: 1

roles:
  analysts:
    description: "Data analysts - read access to non-PII marts"
    member_of: []
    grants:
      - schema: marts
        tables: all
        privileges: [SELECT]
        exclude_columns_tagged: [pii]
      - schema: staging
        tables: [orders, products]
        privileges: [SELECT]

  finance:
    description: "Finance team - read/write on payments schema"
    member_of: []
    grants:
      - schema: payments
        tables: all
        privileges: [SELECT, INSERT, UPDATE]
      - schema: marts
        tables: [revenue_summary]
        privileges: [SELECT]

  interns:
    description: "Interns - restricted read-only sandbox access"
    member_of: []
    grants:
      - schema: sandbox
        tables: all
        privileges: [SELECT]
    deny:
      - schema: patients
        tables: all
        columns_tagged: [pii]

  admin:
    description: "Full platform administrators"
    member_of: [analysts, finance]
    grants: []

tags:
  pii:
    - schema: public
      table: patients
      columns: [name, nin, phone]
    - schema: public
      table: users
      columns: [email, phone_number]

admin inherits everything analysts and finance are granted — no need to redeclare it. Anyone can add a new role by dropping a new entry into roles:; the engine has no hardcoded knowledge of role names.

Safeguards

  • Dry-run by default. plan never writes to the database. apply requires --apply to actually execute.
  • Self-schema protection. A role can never have its own declared schema revoked as a side effect of a diff.
  • Active-session protection. Revoking access from a role with an open database session requires --force.
  • Unmanaged-role isolation. Roles that exist in the database but aren't declared in your config directory are never modified.
  • Atomic apply. All statements in an apply run commit together, or none do — a failure partway through rolls back the whole batch.

CI/CD

All three workflows are implemented as reusable workflows (on: workflow_call) rather than standalone jobs. Each accepts inputs for the policy path, Python version, and target environment, and takes the database connection string as a secret — so any repository (or any environment within this repo) can call them from a thin wrapper workflow instead of duplicating the underlying CI logic.

Workflow Trigger Purpose
plan.yml Called from a wrapper on pull requests touching your config directory (policies/** by default — configurable in the caller workflow) Computes the diff, posts it as a PR comment for review
apply.yml Called from a wrapper on push to main Reconciles the live database to match the merged policy
drift_check.yml Called from a wrapper on a schedule (every 6 hours) Independently checks for drift outside the PR flow, alerts on Slack/GitHub Issues by severity (critical if PII columns affected, else warning or info)

Common inputs exposed by each reusable workflow:

Input Default Description
config-path policies/ Path to the policy directory or file to evaluate — set this to wherever your policies actually live; it isn't required to be policies/
python-version 3.12 Python version used to run the provisioner
environment production Target GitHub environment (controls which secrets/protection rules apply)

Secrets:

Secret Required Description
DATABASE_URL Yes Connection string for the target database

Because they're reusable, each workflow can be invoked with a short caller workflow in the consuming repo, passing config-path, python-version, and environment as needed and forwarding DATABASE_URL via secrets: inherit or an explicit secrets: block.

Each workflow is scoped to the minimum GitHub permissions it needs (issues: write for plan/drift_check; none for apply).

Roadmap

  • Postgres adapters, behind the existing DBAdapter interface
  • Role creation as part of the diff (currently, roles must already exist in the target database)
  • Column-level masking via generated views, for warehouses without native column-level GRANT

Future Improvement

  • Implement other adapters
  • Grant and revoke rate limiting
  • Observability

License

Apache License

About

A Role Based Access tool for databases

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages