Chapter 2. Enterprise Access Control: securing RAG with RBAC/ABAC | Grigoriy Dobryakov

Grigoriy Dobryakov

Course · Enterprise AI Governance Architecture

Chapter 2AI Governance course

Chapter 2. Enterprise Access Control: securing RAG with RBAC/ABAC

A Kovcheg operations clerk asks the assistant: "What were the terms on the last deal with this customer?" The assistant honestly searches the RAG index, finds a document from the legal department — and hands it over. Problem: the clerk has no access to that document in SharePoint. They didn't hack anything; they just asked the bot, and the bot isn't a rights subject. The retriever found the most relevant hit, the model summarized it. This is how AI produces privileged leakage: data the person has no access to at the source flows out through the assistant anyway.

This is a fundamental defect of naive RAG: indexing collapses data at different access levels into a single vector index, and semantic search doesn't know about permissions. This chapter is about the identity plane: RAG must return answers strictly within the rights the user actually has in the source systems (Confluence, Jira, SharePoint, PostgreSQL) — and that must be provable.

The customer's business goal

A Kovcheg user gets from RAG exactly what they're allowed to see in the source systems — no more. Promises to the business:

  1. No answer contains data the asker lacks access to at the source.
  2. Access revocation at the source propagates into RAG within minutes, not days.
  3. When a regulator or auditor asks "why did this employee see this," the answer lies in the permissions, not in the retriever's luck.

Driver: threat or regulator

Architectural pattern

Identity-Aware Hybrid Filtering RAG — the 2026 enterprise-architecture consensus: don't choose between fast pre-filtering and precise authz — combine both:

JWT(user: groups, roles, tenant_id)
        │
        ▼
[1] Pre-filter in the vector DB by ACL metadata (recall, cheap)
        │  candidate chunks
        ▼
[2] FGA post-check: CheckBulkPermissions against each chunk's source document
        │  permitted chunks          (precision, ReBAC/ABAC)
        ▼
[3] LLM  →  [4] OPA post-filter on the answer (field masking, forbidden combinations)

The pre-filter cheaply cuts out the bulk; the FGA service gives a precise check where the payload filter isn't enough (complex relationships, ABAC context). The user's rights penetrate the retrieval itself, rather than being layered onto the output after the fact.

Engineering stack & providers

Engineering implementation

### Step 1. ACL as chunk metadata + version

At indexing time, each chunk's payload gets a normalized ACL array and an acl_version. The version is the key to instant invalidation (see synchronization below).

payload = {
    "tenant_id": doc.tenant_id,
    "allowed_groups": doc.acl_groups,     # from the source system
    "source_doc_id": doc.id,              # for FGA post-check
    "acl_version": doc.acl_version,
}

### Step 2. Pre-filter from the JWT security context

flt = {"must": [
    {"key": "tenant_id",     "match": {"value": user.tenant_id}},
    {"key": "allowed_groups","match": {"any": user.groups}},
]}
candidates = qdrant.search(query_vec, query_filter=flt, limit=50)

### Step 3. FGA post-check against the source

allowed = fga.batch_check(
    user=f"user:{user.id}",
    relation="viewer",
    objects=[f"doc:{c.payload['source_doc_id']}" for c in candidates],
)
chunks = [c for c in candidates if allowed[c.payload["source_doc_id"]]]

### Step 4. ACL sync — event-driven, not batch

Subscribe to permission-change webhooks from the source systems; permission changes reconcile within minutes. Bump the acl_version → stale chunks are invalidated. A nightly batch sync is unacceptable here: a day with a revoked access still valid is a day of leakage.

### Step 5. OPA at the output

The final answer and its citations pass through policy: masking of individual fields, banning dangerous combinations, redacting reference-link metadata.

Where it breaks

Standards and mapping

Lab and artifact

Set up Qdrant with payload ACLs and acl_version for Kovcheg, OIDC via Keycloak, pre-filter by tenant_id+groups, an OpenFGA relationship model + batch_check in the retrieval path, webhook-based access-revocation sync, OPA at the output. Test: two users with different permissions ask the same question from the introduction — compare the results; revoke access and measure reconciliation time. Artifact: FGA model + rego policies + a "role × document → access" test matrix + a sync-lag metric.

Maturity checklist

Sources

In practice

How it actually works — engineering breakdowns

Standalone howto from practice, showing this control plane on real code and a working artifact.

Read next

Putting AI into production under regulatory risk?

Designing the control plane for your system: privacy, access, guardrails, audit, EU AI Act / ISO 42001 compliance — as working architecture, not a policy PDF.

Email me

The transition engine

Next Move Engine — the system that takes a team to an autonomous delivery loop.

Next Move Engine →