We use GenAI in every facet of technology now – internal knowledge bases, customer support systems, and code review bots, to name just a few use cases. And in nearly every one of these, someone eventually asks:
What stops the model from returning something the user shouldn’t see?”
This is a roadblock that companies building RAG features or AI Agents eventually hit – the moment where an LLM returns data from a document that the user was not authorized to access, introducing potential legal, financial, and reputational risk to all parties. Unfortunately, traditional methods of authorization are not suited for the hierarchical, dynamic nature of access control in RAG. This is exactly where modern authorization permissioning systems such as SpiceDB shine: in building fine-grained authorization for filtering content in your AI-powered applications.
In fact, OpenAI uses SpiceDB to secure 37 Billion documents for 5 Million users who use ChatGPT Connectors – a feature where you bring your data from different sources such as Google Drive, Dropbox, GitHub etc. into ChatGPT.
This blog post shows how you can pair SpiceDB with Testcontainers to give you the ability to test your permission logic inside your RAG pipeline, end-to-end, automatically, with zero infrastructure dependencies.The example repo can be found here.
Quick Primer on Authorization
Before diving into implementation, let’s clarify two foundational concepts: Authentication (verifying who a user is) and Authorization (deciding what they can access).
Authorization is commonly implemented via techniques such as:
- Access Control Lists (ACLs)
- Role-Based Access Control (RBAC)
- Attribute-Based Access Control (ABAC)
However, for complex, dynamic, and context-rich applications like RAG pipelines, traditional methods such as RBAC or ABAC fall short. The new kid on the block – ReBAC (Relationship-Based Access Control) is ideal as it models access as a graph of relationships rather than fixed rules, providing the necessary flexibility and scalability required.
ReBAC was popularized in Google Zanzibar, the internal authorization system Google built to manage permissions across all its products (e.g., Google Docs, Drive). Zanzibar systems are optimized for low-latency, high-throughput authorization checks, and global consistency – requirements that are well-suited for RAG systems.
SpiceDB is the most scalable open-source implementation of Google’s Zanzibar authorization model. It stores access as a relationship graph, where the fundamental check reduces to:
Is this actor allowed to perform this action on this resource?
For a Google Docs-style example:
definition user {}
definition document {
relation reader: user
relation writer: user
permission read = reader + writer
permission write = writer
}
This schema defines object types (user and document), explicit Relations between the objects (reader, writer), and derived Permissions (read, write). SpiceDB evaluates the relationship graph in microseconds, enabling real-time authorization checks at massive scale.
Access Control for RAG
RAG (Retrieval-Augmented Generation) is an architectural pattern that enhances Large Language Models (LLMs) by letting them consult an external knowledge base, typically involving a Retriever component finding document chunks and the LLM generating an informed response.
This pattern is now used by businesses and enterprises for apps like chatbots that query sensitive data such as customer playbooks or PII – all stored in a vector database for performance. However, the fundamental risk in this flow is data leakage: the Retriever component ignores permissions, and the LLM will happily summarize unauthorized data. In fact, OWASP has a Top 10 Risks for Large Language Model Applications list which includes Sensitive Information Disclosure, Excessive Agency & Vector and Embedding Weaknesses. The consequences of this leakage can be severe, ranging from loss of customer trust to massive financial and reputational damage from compliance violations.
This setup desperately needs fine-grained authorization, and that’s where SpiceDB comes in. SpiceDB can post-filter retrieved documents by performing real-time authorization checks, ensuring the model only uses data the querying user is permitted to see. The only requirement is that the documents have metadata that indicates where the information came from.But testing this critical permission logic without mocks, manual Docker setup, or flaky Continuous Integration (CI) environments is tricky. Testcontainers provides the perfect solution, allowing you to spin up a real, production-grade, and disposable SpiceDB instance inside your unit tests to deterministically verify that your RAG pipeline respects permissions end-to-end.
Spin Up Real Authorization for Every Test
Instead of mocking your authorization system or manually running it on your workstation, you can add this line of code in your test:
container, _ := spicedbcontainer.Run(ctx, "authzed/spicedb:v1.47.1")
And Testcontainers will:
- Pull the real SpiceDB image
- Start it in a clean, isolated environment
- Assign it dynamic ports
- Wait for it to be ready
- Hand you the gRPC endpoint
- Clean up afterwards
Because Testcontainers handles the full lifecycle – from pulling the container, exposing dynamic ports, and tearing it down automatically, you eliminate manual processes such as running Docker commands, and writing cleanup scripts. This isolation ensures that every single test runs with a fresh, clean authorization graph, preventing data conflicts, and making your permission tests completely reproducible in your IDE and across parallel Continuous Integration (CI) builds.
Suddenly you have a real, production-grade, Zanzibar-style permissions engine inside your unit test.
Using SpiceDB & Testcontainers
Here’s a walkthrough of how you can achieve end-to-end permissions testing using SpiceDB and Testcontainers. The source code for this tutorial can be found here.
1. Testing Our RAG
For the sake of simplicity, we have a minimal RAG and the retrieval mechanism is trivial too.
We’re going to test three documents which have doc_ids (doc1 doc2 ..) that act as metadata.
doc1: Internal roadmapdoc2: Customer playbookdoc3: Public FAQ
And three users:
- Emilia owns
doc1 - Beatrice can view
doc2 - Charlie (or anyone) can view
doc3
This SpiceDB schema defines a user and a document object type. A user has read permission on a document if they are the direct viewer or the owner of the document.
definition user {}
definition document {
relation owner: user
relation viewer: user | owner
permission read = owner + viewer
}
2. Starting the Testcontainer
Here’s how a line of code can start a test to launch the disposable SpiceDB instance:
container, err := spicedbcontainer.Run(ctx, "authzed/spicedb:v1.47.1")
require.NoError(t, err)
Next, we connect to the running containerized service:
host, _ := container.Host(ctx)
port, _ := container.MappedPort(ctx, "50051/tcp")
endpoint := fmt.Sprintf("%s:%s", host, port.Port())
client, err := authzed.NewClient(
endpoint,
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpcutil.WithInsecureBearerToken("somepresharedkey"),
)
This is now a fully-functional SpiceDB instance running inside your test runner.
3. Load the Schema + Test Data
The test seeds data the same way your application would:
_, err := client.WriteSchema(ctx, &apiv1.WriteSchemaRequest{Schema: schema})
require.NoError(t, err)
Then:
rel("document", "doc1", "owner", "user", "emilia")
rel("document", "doc2", "viewer", "user", "beatrice")
rel("document", "doc3", "viewer", "user", "emilia")
rel("document", "doc3", "viewer", "user", "beatrice")
rel("document", "doc3", "viewer", "user", "charlie")
We now have a predictable, reproducible authorization graph for every test run.
4. Post-Filtering With SpiceDB
Before the LLM sees anything, we check permissions with SpiceDB which acts as the source of truth of the permissions in the documents.
resp, err := r.spiceClient.CheckPermission(ctx, &apiv1.CheckPermissionRequest{
Resource: docObject,
Permission: "read",
Subject: userSubject,
})
If SpiceDB says no, the doc is never fed into the LLM, thereby ensuring the user gets an answer to their query only based on what they have permissions to read.
This avoids:
- Accidental data leakage
- Overly permissive vector search
- Compliance problems
Traditional access controls break down when data becomes embeddings hence having guardrails prevents this from happening.
End-to-End Permission Checks in a Single Test
Here’s what the full test asserts:
Emilia queries “roadmap” → gets doc1
Because they’re the owner.
Beatrice queries “playbook” → gets doc2
Because she’s a viewer.
Charlie queries “public” → gets doc3
Because it’s the only doc he can read, as it’s a public doc
If there is a single failing permission rule, the end-to-end test will immediately fail, which is critical given the constant changes in RAG pipelines (such as new retrieval modes, embeddings, document types, or permission rules).
What If Your RAG Pipeline Isn’t in Go?
First, a shoutout to Guillermo Mariscal for his original contribution to the SpiceDB Go Testcontainers module.
What if your RAG pipeline is written in a different language such as Python? Not to worry, there’s also a community Testcontainers module written in Python that you can use similarly. The module can be found here.
Typically, you would integrate it in your integration tests like this:
# Your RAG pipeline test
def test_rag_pipeline_respects_permissions():
with SpiceDBContainer() as spicedb:
# Set up permissions schema
client = create_spicedb_client(
spicedb.get_endpoint(),
spicedb.get_secret_key()
)
# Load your permissions model
client.WriteSchema(your_document_permission_schema)
# Write test relationships
# User A can access Doc 1
# User B can access Doc 2
# Test RAG pipeline with User A
results = rag_pipeline.search(query="...", user="A")
assert "Doc 1" in results
assert "Doc 2" not in results # Should be filtered out!
Similar to the Go module, this container gives you a clean, isolated SpiceDB instance for every test run.
Why This Approach Matters
Authorization testing in RAG pipelines can be tricky, given the scale and latency requirement and it can get trickier in systems handling sensitive data. By integrating the flexibility and scale of SpiceDB with the automated, isolated environments of Testcontainers, you shift to a completely reliable, deterministic approach to authorization.
Every time your code ships, a fresh, production-grade authorization engine is spun up, loaded with test data, and torn down cleanly, guaranteeing zero drift between your development machine and CI. This pattern can ensure that your RAG system is safe, correct, and permission-aware as it scales from three documents to millions.
Try It Yourself
The complete working example in Go along with a sample RAG pipeline is here:
https://github.com/sohanmaheshwar/spicedb-testcontainer-rag
Clone it.
Run go test -v.
Watch it spin up a fresh SpiceDB instance, load permissions, and assert RAG behavior.
Also, find the community modules for the SpiceDB testcontainer in Go and Python.