diff --git a/app/spicedb/_meta.ts b/app/spicedb/_meta.ts index 8776ed47..9be8212d 100644 --- a/app/spicedb/_meta.ts +++ b/app/spicedb/_meta.ts @@ -1,8 +1,10 @@ export default { "getting-started": "Getting Started", concepts: "Concepts", - modeling: "Modeling & Integrating", + modeling: "Modeling", ops: "Operations", + integrations: "Integrations", + tutorials: "Tutorials", api: "API Reference", links: "Links", }; diff --git a/app/spicedb/integrations/_meta.ts b/app/spicedb/integrations/_meta.ts new file mode 100644 index 00000000..503600d2 --- /dev/null +++ b/app/spicedb/integrations/_meta.ts @@ -0,0 +1,6 @@ +export default { + "langchain-spicedb": + "Use SpiceDB with LangChain & LangGraph for RAG & AI Agent Authorization", + pinecone: "Access Control in RAG with Pinecone and SpiceDB", + testcontainers: "Testing RAG Pipelines with Testcontainers", +}; diff --git a/app/spicedb/integrations/langchain-spicedb/page.mdx b/app/spicedb/integrations/langchain-spicedb/page.mdx new file mode 100644 index 00000000..62f9acf6 --- /dev/null +++ b/app/spicedb/integrations/langchain-spicedb/page.mdx @@ -0,0 +1,519 @@ +import { Callout } from "nextra/components"; + +# Use SpiceDB with LangChain & LangGraph for RAG & AI Agent Authorization + +This guide explains how to enforce fine-grained, per-document authorization in Retrieval-Augmented Generation (RAG) pipelines or with AI Agents, using the official [LangChain SpiceDB](https://pypi.org/project/langchain-spicedb/) library + +Retrieval-Augmented Generation (RAG) systems combine vector search with large language models to answer questions using your organization's knowledge base. +However, without proper authorization controls, these systems can inadvertently leak sensitive information to unauthorized users. + +The Langchain-SpiceDB library integrates SpiceDB's fine-grained authorization directly into LangChain / LangGraph pipelines by implementing **post-filter authorization**. Documents are first retrieved based on semantic similarity, then filtered through SpiceDB permission checks before being passed to your LLM. This maintains the quality of vector search while ensuring strict authorization controls. + +## Installation + +Install the base library using pip: + +```bash +pip install langchain-spicedb +``` + +## Choosing the Right Component + +The library provides four main components, each designed for different use cases. +Full examples can be found in the repository linked at the bottom of this page. + +| Component | Use Case | Best For | +| ----------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------- | -------------------------------------------------------------------------------------------- | +| [**SpiceDBRetriever**](https://github.com/authzed/langchain-spicedb/blob/main/docs/langchain-integration/langchain-spicedb-retriever.mdx) | Simple RAG pipelines | Drop-in replacement for any retriever. Wraps your existing retriever with authorization. | +| [**SpiceDBAuthFilter**](https://github.com/authzed/langchain-spicedb/blob/main/langchain_spicedb/langchain_runnable.py) | LangChain chains with middleware | Filtering documents in the middle of a chain. Reusable across different users via `config`. | +| [**LangGraph Node**](https://github.com/authzed/langchain-spicedb/blob/main/docs/langgraph-guide.md) | LangGraph workflows | Complex multi-step workflows with state management. Provides authorization metrics in state. | +| [**Permission Tools**](https://github.com/authzed/langchain-spicedb/blob/main/docs/langchain-integration/langchain-spicedb-tools.mdx#spicedbpermissiontool) | Agentic workflows | Give agents the ability to check permissions before taking actions. | + +Let's explore each component in detail. + +## 1. SpiceDBRetriever + +`SpiceDBRetriever` wraps any existing LangChain retriever and adds authorization filtering. +This is the simplest approach when building RAG chains for a single user context. + +```python +from langchain_spicedb import SpiceDBRetriever + +retriever = SpiceDBRetriever( + base_retriever=vector_store.as_retriever(), # works with any vector DB + subject_id="alice", + subject_type="user", + spicedb_endpoint="localhost:50051", + spicedb_token="sometoken", + resource_type="article", + resource_id_key="article_id", + permission="view", +) + +docs = await retriever.ainvoke("query") +``` + +The retriever fetches documents using your vector store's semantic search, then checks SpiceDB permissions for each document. +Only documents that `alice` has the `view` permission for are returned. + + + Each document's metadata must include a field (specified by `resource_id_key`) + that contains the resource identifier used in SpiceDB. This is how the library + knows which SpiceDB resource to check permissions against. + + +**When to use this approach:** + +- You're building a simple RAG chain with a fixed user context +- The user doesn't change between invocations +- You want the simplest possible integration + +## 2. SpiceDBAuthFilter + +`SpiceDBAuthFilter` provides a reusable filter component that accepts the user context at runtime. +This allows you to build a single chain that serves multiple users, each receiving answers based only on documents they can access. + +```python +from langchain_spicedb import SpiceDBAuthFilter +from langchain_core.runnables import RunnableParallel, RunnablePassthrough +from langchain_core.output_parsers import StrOutputParser + +# Create the authorization filter (no user specified yet) +auth_filter = SpiceDBAuthFilter( + spicedb_endpoint="localhost:50051", + spicedb_token="sometoken", + resource_type="document", + resource_id_key="doc_id", +) + +# Build a reusable chain +prompt = ChatPromptTemplate.from_messages([ + ("system", "Answer based only on the provided context."), + ("human", "Context: {context}\n\nQuestion: {question}") +]) + +chain = ( + RunnableParallel({ + "context": retriever | auth_filter, # Authorization happens here + "question": RunnablePassthrough(), + }) + | prompt + | llm # needs to be defined + | StrOutputParser() +) + +# Invoke with different users by passing subject_id in config +answer_alice = await chain.ainvoke( + "What are the Q4 results?", + config={"configurable": {"subject_id": "user:alice"}} +) + +answer_bob = await chain.ainvoke( + "What are the Q4 results?", + config={"configurable": {"subject_id": "user:bob"}} +) +``` + +Alice and Bob each receive answers based only on the Q4 documents they're authorized to view. +The same chain handles both requests, with authorization determined by the `subject_id` passed in the config. + +**When to use this approach:** + +- You're building a multi-tenant RAG system +- The same chain needs to serve many different users +- You want to reuse chains across requests while maintaining per-user authorization + +## 3. LangGraph Authorization Node + +For complex, multi-step RAG workflows, LangGraph provides explicit control over each stage of your pipeline. +The LangChain-SpiceDB library includes a state definition and authorization node factory designed specifically for LangGraph integration. + +```python +from langgraph.graph import StateGraph, END +from langchain_spicedb import create_auth_node, RAGAuthState +from langchain_openai import ChatOpenAI +from langchain_core.prompts import ChatPromptTemplate + +# Use the provided state definition +graph = StateGraph(RAGAuthState) + +def retrieve_node(state): + """Retrieve documents from vector store""" + docs = retriever.invoke(state["question"]) + return {"retrieved_documents": docs} + +def generate_node(state): + """Generate answer from authorized documents""" + # Only authorized documents are available here + context = "\n\n".join([ + doc.page_content + for doc in state["authorized_documents"] + ]) + + prompt = ChatPromptTemplate.from_messages([ + ("system", "Answer based only on the provided context."), + ("human", "Question: {question}\n\nContext: {context}") + ]) + + llm = ChatOpenAI(model="gpt-4") + messages = prompt.format_messages( + question=state["question"], + context=context + ) + answer = llm.invoke(messages) + + return {"answer": answer.content} + +# Add nodes to graph +graph.add_node("retrieve", retrieve_node) +graph.add_node("authorize", create_auth_node( + spicedb_endpoint="localhost:50051", + spicedb_token="sometoken", + resource_type="article", + resource_id_key="article_id", +)) +graph.add_node("generate", generate_node) + +# Define edges +graph.set_entry_point("retrieve") +graph.add_edge("retrieve", "authorize") +graph.add_edge("authorize", "generate") +graph.add_edge("generate", END) + +# Compile and run +app = graph.compile() +result = await app.ainvoke({ + "question": "What is SpiceDB?", + "subject_id": "user:alice", +}) + +print(result["answer"]) +``` + +The authorization node reads documents from `state["retrieved_documents"]`, checks permissions, and writes authorized documents to `state["authorized_documents"]`. +Authorization metrics are automatically included in `state["auth_results"]`. + + + LangGraph is ideal when you need explicit control over each pipeline step, + want to maintain state across multiple conversation turns, or need complex + branching logic based on authorization results. + + +**When to use this approach:** + +- You need multi-step RAG pipelines (retrieval, reranking, generation, etc.) +- Your workflow includes conditional branching based on authorization +- You want explicit visibility into each stage of processing +- You're building conversational systems that maintain state across turns + +### Extending RAGAuthState + +The base `RAGAuthState` includes fields for questions, documents, and authorization results. +You can extend it to add custom fields to track additional state like conversation history, user preferences, or metadata. + +```python +from langchain_spicedb import RAGAuthState +from typing import List + +class CustomerSupportState(RAGAuthState): + conversation_history: List[dict] + customer_tier: str + sentiment_score: float + +graph = StateGraph(CustomerSupportState) + +def personalized_generate(state): + """Generate response considering customer context""" + tier = state["customer_tier"] + sentiment = state["sentiment_score"] + + # Adjust response based on custom state + # ... your generation logic + + return {"answer": response} +``` + +This pattern allows you to combine authorization with other application-specific state management needs. +For more details on what the LangGraph library does, check out [the docs](https://github.com/authzed/langchain-spicedb/blob/main/docs/langgraph-guide.md) in the repository + +## 4. Permission Check Tools + +For agentic workflows where an AI agent makes decisions based on permissions, the library provides permission-checking tools that can be used directly by agents or invoked programmatically as part of application logic. + +These tools allow agents to reason about who can do what before responding or taking action. + +#### SpiceDBPermissionTool + +Use this tool to check whether a subject has a specific permission on a single resource. + +```python +from langchain_spicedb import SpiceDBPermissionTool + +tool = SpiceDBPermissionTool( + spicedb_endpoint="localhost:50051", + spicedb_token="sometoken", + subject_type="user", + resource_type="article", +) + +result = await tool.ainvoke({ + "subject_id": "alice", + "resource_id": "123", + "permission": "view" +}) +# Returns: "true" or "false" +``` + +#### SpiceDBBulkPermissionTool + +Same as `SpiceDBPermissionTool` but check permissions for multiple resources at once: + +```python +from langchain_spicedb import SpiceDBBulkPermissionTool + +tool = SpiceDBBulkPermissionTool( + spicedb_endpoint="localhost:50051", + spicedb_token="sometoken", + subject_type="user", + resource_type="article", +) + +result = await tool.ainvoke({ + "subject_id": "tim", + "resource_ids": "123,456,789", + "permission": "view" +}) +# Returns: "tim can access: 123, 456" or "tim cannot access any..." +``` + +When used with LangChain agents, these tools allow the agent to dynamically check permissions as part of its reasoning process, enabling authorization-aware responses and workflows. +Typical use cases include: + +Building autonomous agents that must check permissions at runtime + +- Making permission checks part of decision-making or tool selection +- Explaining why access is allowed or denied in user-facing responses +- This approach ensures that authorization logic remains centralized in SpiceDB while still being accessible to AI-driven workflows. + +In agentic workflows, permission tools can be registered as agent tools, allowing the agent to dynamically check authorization before responding or taking action. + +```python +from langchain.agents import create_react_agent, AgentExecutor +from langchain_openai import ChatOpenAI +from langchain_spicedb import SpiceDBPermissionTool + +llm = ChatOpenAI(model="gpt-4o-mini") + +permission_tool = SpiceDBPermissionTool( + spicedb_endpoint="localhost:50051", + spicedb_token="sometoken", + subject_type="user", + resource_type="article", +) + +tools = [permission_tool] + +agent = create_agent( + llm=llm, + tools=tools, + system_prompt=( + "You are a helpful assistant. " + "Before providing information about a resource, " + "check whether the user has the required permission." + ), +) + +result = await agent.ainvoke( + {"messages": [{"role": "user", "content": "Can user tim view article 123?"}]} + ) + print(f"\nAgent Response:\n{result['messages'][-1].content}") +``` + +## Document Metadata Requirements + +This technique of post=filter authorization requires each document in your vector store to include a resource identifier in its metadata. +This identifier maps the document to its corresponding resource in SpiceDB. + +```python +from langchain_core.documents import Document + +# Document with required metadata +doc = Document( + page_content="SpiceDB is an open-source permissions database...", + metadata={ + "doc_id": "123", # Resource identifier + "title": "SpiceDB Introduction", + "author": "alice" + } +) +``` + +When configuring SpiceDB components, the `resource_id_key` parameter specifies which metadata field contains the resource ID: + +```python +auth_filter = SpiceDBAuthFilter( + spicedb_endpoint="localhost:50051", + spicedb_token="sometoken", + resource_type="document", + resource_id_key="123", # Matches metadata key +) +``` + +The library checks permissions for `document:123` when evaluating this document. +If `resource_id_key` doesn't match a field in the document's metadata, that document will be filtered out. + + + Ensure all documents in your vector store include the resource ID field. + Documents missing this field will be silently excluded from results, which can + lead to incomplete answers without explicit errors. + + +## Authorization Metrics + +Understanding how authorization affects your RAG pipeline is crucial for debugging and optimization. +All components provide detailed metrics about the authorization process. + +### LangChain Components + +Enable metrics by setting `return_metrics=True`: + +```python +from langchain_spicedb import SpiceDBAuthFilter + +auth_filter = SpiceDBAuthFilter( + spicedb_endpoint="localhost:50051", + spicedb_token="sometoken", + resource_type="document", + resource_id_key="doc_id", + subject_id="user:alice", + return_metrics=True +) + +result = await auth_filter.ainvoke(documents) + +# Access metrics +print(f"Documents retrieved: {result.total_retrieved}") +print(f"Documents authorized: {result.total_authorized}") +print(f"Authorization rate: {result.authorization_rate:.1%}") +print(f"Denied documents: {result.denied_resource_ids}") +print(f"Check latency: {result.check_latency_ms}ms") +``` + +### LangGraph Components + +Metrics are automatically included in the graph state under the `auth_results` key: + +```python +result = await app.ainvoke({ + "question": "What is SpiceDB?", + "subject_id": "user:alice", +}) + +# Access metrics from state +auth_metrics = result["auth_results"] +print(f"Total retrieved: {auth_metrics['total_retrieved']}") +print(f"Total authorized: {auth_metrics['total_authorized']}") +print(f"Authorization rate: {auth_metrics['authorization_rate']:.1%}") +print(f"Denied IDs: {auth_metrics['denied_resource_ids']}") +print(f"Latency: {auth_metrics['check_latency_ms']}ms") +``` + +**What these metrics tell you:** + +- **Authorization rate**: Percentage of retrieved documents the user can access. Low rates suggest over-fetching from your vector store. +- **Denied resource IDs**: Specific documents filtered out. Useful for debugging permission issues. +- **Check latency**: Time spent on authorization. Helps identify performance bottlenecks. +- **Total counts**: Raw numbers for monitoring and alerting. + +Monitor these metrics to optimize your retrieval strategy and ensure authorization performance meets your requirements. + +## Production Deployment + +When deploying to production, configure your SpiceDB client for security and reliability: + +```python +from authzed.api.v1 import Client + +# Production client configuration +spicedb_client = Client( + "spicedb.production.example.com:443", + "your-production-token", + cert_path="/path/to/ca-cert.pem", # TLS certificate + timeout_seconds=5.0, # Request timeout +) +``` + +### Vector Store Compatibility + +The library works with any vector store that implements the LangChain retriever interface. +Authorization is applied after retrieval, making your choice of vector store independent of authorization behavior. + +**Compatible vector stores include:** + +- [Pinecone](https://python.langchain.com/docs/integrations/vectorstores/pinecone) +- [FAISS](https://python.langchain.com/docs/integrations/vectorstores/faiss) +- [Weaviate](https://python.langchain.com/docs/integrations/vectorstores/weaviate) +- [Chroma](https://python.langchain.com/docs/integrations/vectorstores/chroma) +- [Qdrant](https://python.langchain.com/docs/integrations/vectorstores/qdrant) +- Any other LangChain-compatible vector store + +This flexibility allows you to choose the best vector store for your use case without compromising authorization capabilities. + +### Error Handling + + + Never fall back to unfiltered results when authorization fails. This creates a + path for information leakage during system outages. Instead, return an error + to the user or empty results with an explanation. + + +```python +from langchain_spicedb import SpiceDBAuthFilter +from authzed.api.v1 import AuthzedError + +try: + result = await auth_filter.ainvoke(documents) +except AuthzedError as e: + # Handle SpiceDB connectivity or permission errors + logger.error(f"Authorization failed: {e}") + # Fail closed: don't return unauthorized documents + result = [] +``` + +### Post-Filter vs Metadata Filtering + +Some vector stores support metadata filtering (e.g., adding a `user_id` field to each document). +This approach has significant limitations: + +**Metadata filtering challenges:** + +- Requires duplicating authorization logic in your vector store +- Cannot express relationship-based permissions (team membership, hierarchies) +- Difficult to maintain with complex permission models +- No audit trail of authorization decisions +- Requires re-indexing when permissions change + +### Debug Logging + +Enable debug logging to troubleshoot authorization issues: + +```python +import logging + +logging.basicConfig(level=logging.DEBUG) +logger = logging.getLogger("langchain_spicedb") +logger.setLevel(logging.DEBUG) +``` + +Debug logs include: + +- Authorization requests and responses +- Document filtering decisions +- Performance metrics +- Detailed error information + +## Next Steps + +- [LangChain Documentation](https://python.langchain.com/) - Official LangChain docs +- Check out the [library repository](https://github.com/authzed/langchain-spicedb) for examples and source code +- [PyPi page](https://pypi.org/project/langchain-spicedb/) for `langchain-spicedb` library diff --git a/app/spicedb/integrations/pinecone/page.mdx b/app/spicedb/integrations/pinecone/page.mdx new file mode 100644 index 00000000..7b622710 --- /dev/null +++ b/app/spicedb/integrations/pinecone/page.mdx @@ -0,0 +1,202 @@ +import { Callout } from "nextra/components"; + +# Access Control in RAG with Pinecone and SpiceDB + +Retrieval-Augmented Generation (RAG) systems combine the power of vector search with large language models to answer questions using your organization's data. +However, without proper authorization controls, these systems can leak sensitive information across organizational boundaries. + +This guide shows how to implement fine-grained, relationship-based authorization in RAG pipelines using SpiceDB and [Pinecone](https://www.pinecone.io/) - a fully managed, cloud-native vector database designed for fast similarity searches at scale. By integrating SpiceDB's permission checks directly into your retrieval workflow, you ensure that users only receive answers based on documents they're authorized to access. + +## Why Authorization Matters in RAG + +Standard RAG pipelines retrieve documents based purely on semantic similarity, without considering user permissions. +This creates critical security vulnerabilities that the OWASP Foundation identifies as [Top 10 risks for LLM applications](https://owasp.org/www-project-top-10-for-large-language-model-applications/). + +**The risks include:** + +- **Sensitive Information Disclosure**: Users may receive answers containing data from unauthorized documents +- **Excessive Agency**: Actions to be performed in response to unexpected, ambiguous or manipulated outputs from an LLM +- **Vector embedding weaknesses**: Embeddings themselves can encode sensitive information + +Consider a common scenario: your company stores both public marketing documents and confidential financial reports in the same vector database. +Without authorization, an entry-level employee asking "What was our Q4 revenue?" might receive an answer derived from executive-only financial documents—even if the employee never had direct access to those files. + +Authorization isn't just about blocking document access after retrieval. +It must be enforced throughout the entire RAG pipeline to prevent information leakage through embeddings, metadata, and generated responses and that's where SpiceDB comes in. Read more about ReBAC, Google Zanzibar, and SpiceDB [here](https://authzed.com/docs/spicedb/getting-started/discovering-spicedb). + +## Intro to SpiceDB + +SpiceDB stores access relationships as a graph, where nodes represent entities (users, groups, documents) and edges represent relationships (like “viewer,” “editor,” or “owner”). Fundamentally, authorization logic can be reduced to asking a single question: + +_Is this actor allowed to perform this action on this resource?_ + +In SpiceDB parlance, this actor and this resource are both Objects and this action is a Permission or Relation. Here’s a Google Docs style example where a user can be either a reader or a writer of a document. A reader can only read the document, whereas a writer can read and write the document. + +You can represent this use case using a schema like this: + +```zed +definition user {} + +definition article { + relation viewer: user + relation editor: user + + permission view = viewer + editor + permission edit = editor +} +``` + +In this model, articles have two relations: `viewer` (users who can read) and `editor` (users who can modify). +The `view` permission is granted to anyone in either relation, while `edit` requires the `editor` relation. + + + Relations define direct relationships between objects, while permissions + compute who has access based on those relations. You cannot write + relationships directly to permissions—only to relations. + + +## Authorization Strategies: Pre-Filter vs Post-Filter + +There are two primary approaches to enforcing authorization in RAG pipelines. +Each has distinct trade-offs based on your document corpus size, user access patterns, and performance requirements. + +### Pre-Filter Authorization + +Pre-filter authorization queries SpiceDB **before** searching the vector database. +You first ask SpiceDB "which documents can this user access?" and then retrieve only those authorized documents. + +**How it works:** + +1. Call SpiceDB's `LookupResources` API to get all document IDs the user can access +2. Use those IDs to filter the Pinecone query (via metadata filtering) +3. Only authorized documents are retrieved and embedded in the LLM context + +![rag-pre-filter](/images/rag-pre-filter.png) + +**Code example:** + +```python +from authzed.api.v1 import Client, LookupResourcesRequest, SubjectReference, ObjectReference + +# Query SpiceDB for authorized document IDs +client = Client("spicedb.example.com:443", bearer_token) + +subject = SubjectReference( + object=ObjectReference(object_type="user", object_id="alice") +) + +response = client.LookupResources( + LookupResourcesRequest( + resource_object_type="article", + permission="view", + subject=subject + ) +) + +authorized_article_ids = [r.resource_object_id async for r in response] +# Result: ['123', '456', '789'] + +# Use authorized IDs to filter Pinecone query +from pinecone import Pinecone +pc = Pinecone(api_key="your-api-key") +index = pc.Index("documents") + +results = index.query( + vector=query_embedding, + filter={"article_id": {"$in": authorized_article_ids}}, + top_k=10, + include_metadata=True +) +``` + +**When to use pre-filter:** + +- Large document corpus with hundreds of thousands or millions of documents +- Users typically have access to a small percentage of total documents +- Low retrieval hit-rate (most searches return few relevant documents) +- You want predictable authorization overhead independent of search results + +**Trade-offs:** + +- More computationally expensive per authorization check (must enumerate all accessible documents) +- Authorization latency scales with the number of documents a user can access +- Highly efficient when users have narrow access (e.g., team-specific documents) + +### Post-Filter Authorization + +Post-filter authorization retrieves documents from Pinecone **first**, then filters results through SpiceDB permission checks. + +**How it works:** + +1. Query Pinecone normally to retrieve semantically relevant documents +2. Extract document IDs from the results +3. Call SpiceDB's `CheckBulkPermissions` API to verify which documents the user can access +4. Filter out unauthorized documents before passing to the LLM + +![rag-post-filter](/images/rag-post-filter.png) + +**Code example:** + +```python +from authzed.api.v1 import Client, CheckBulkPermissionsRequest, CheckBulkPermissionsItem +from authzed.api.v1 import SubjectReference, ObjectReference, Relationship + +# First: Retrieve documents from Pinecone +from pinecone import Pinecone +pc = Pinecone(api_key="your-api-key") +index = pc.Index("documents") + +results = index.query( + vector=query_embedding, + top_k=20, # Fetch more than needed to account for filtering + include_metadata=True +) + +# Second: Check permissions for each retrieved document +client = Client("spicedb.example.com:443", bearer_token) + +check_items = [ + CheckBulkPermissionsItem( + resource=ObjectReference( + object_type="article", + object_id=match["metadata"]["article_id"] + ), + permission="view", + subject=SubjectReference( + object=ObjectReference(object_type="user", object_id="alice") + ) + ) + for match in results["matches"] +] + +permission_response = await client.CheckBulkPermissions( + CheckBulkPermissionsRequest(items=check_items) +) + +# Third: Filter to only authorized documents +authorized_documents = [ + match for match, pair in zip(results["matches"], permission_response.pairs) + if pair.item.permissionship == CheckBulkPermissionsResponseItem.PERMISSIONSHIP_HAS_PERMISSION +] +``` + +**When to use post-filter:** + +- Smaller document corpus +- High search hit-rate (most retrieved documents are relevant) +- Users typically have broad access to documents +- You want to maintain optimal vector search quality + +**Trade-offs:** + +- Requires over-fetching documents to ensure you have enough after filtering +- Authorization latency scales with number of search results (not total corpus) +- More efficient when users have broad access patterns + +## Related Resources + +For deeper exploration of SpiceDB and RAG authorization: + +- [LangChain-SpiceDB Official Library](https://pypi.org/project/langchain-spicedb/): Post-filter authorization library for LangChain and LangGraph +- [Secure RAG Pipelines Repo & Workshop ](https://github.com/authzed/workshops/tree/main/secure-rag-pipelines): A hands-on self-guided workshop covering both pre-filter and post-filter techniques using Pinecone and Langchain +- [Pinecone Documentation](https://docs.pinecone.io/): Official Pinecone vector database documentation diff --git a/app/spicedb/integrations/testcontainers/page.mdx b/app/spicedb/integrations/testcontainers/page.mdx new file mode 100644 index 00000000..bd3948fc --- /dev/null +++ b/app/spicedb/integrations/testcontainers/page.mdx @@ -0,0 +1,147 @@ +import { Callout } from "nextra/components"; + +# Testing RAG Pipelines with Testcontainers and SpiceDB + +End-to-end testing of permission-aware RAG (Retrieval-Augmented Generation) systems requires validating that authorization logic works correctly before production. +Testing with real SpiceDB instances ensures your permission checks behave exactly as they will in production, catching authorization bugs early in development. + +This guide demonstrates how to use [Testcontainers](https://testcontainers.com/) with SpiceDB to create isolated, reproducible integration tests for RAG pipelines. +Testcontainers is an open source library for providing throwaway, lightweight instances of databases, message brokers, web browsers, or just about anything that can run in a Docker container. + +You'll learn how to verify that users only receive answers based on documents they're authorized to access, preventing data leakage through comprehensive automated testing. + +## Why Test Permissions in RAG Pipelines + +RAG systems retrieve documents from vector databases to augment LLM responses with your organization's data. +Without proper authorization testing, these systems risk exposing sensitive information to unauthorized users. + +### The Testing Gap + +Many RAG implementations test retrieval quality but skip authorization testing entirely: + +```python +# What most tests verify +def test_retrieval_quality(): + results = rag_pipeline.query("quarterly revenue") + assert "Q4 revenue" in results # Tests semantic relevance + +# What tests should also verify +def test_retrieval_authorization(): + results = rag_pipeline.query("quarterly revenue", user="intern") + assert "Q4 revenue" not in results # Tests permission boundaries + assert "public FAQ" in results # Tests authorized access +``` + +Without authorization testing, you won't discover if users can access information from unauthorized documents until in production. + +## Understanding Testcontainers + +Testcontainers is a testing library that provides lightweight, throwaway containers for integration tests. +Instead of mocking authorization logic or maintaining shared test infrastructure, you spin up real SpiceDB instances during tests. + +### How Testcontainers Works + +Each test gets a fresh, isolated SpiceDB container: + +1. **Automatic startup**: Test framework pulls the SpiceDB image and starts a container +2. **Port mapping**: Container ports are dynamically mapped to avoid conflicts +3. **Health checks**: Framework waits for SpiceDB to be ready before running tests +4. **Automatic cleanup**: Container is destroyed after tests complete + +This approach eliminates flaky tests caused by shared state, manual setup, or mock inconsistencies. + + + Testcontainers requires Docker to be running on your system. Install Docker + Desktop or Docker Engine before running tests. The framework automatically + handles all container lifecycle management. + + +## Setting Up Tests with Testcontainers + +The SpiceDB Testcontainer is available in [Python](https://testcontainers.com/modules/spicedb/?language=python) and [Go](https://testcontainers.com/modules/spicedb/?language=go). This guide shows how you can use the Python module. +Python RAG systems built with LangChain, LlamaIndex, or custom implementations can use this for testing. + +### Installing the Python Module + +Install the SpiceDB Testcontainer for Python: + +```bash +pip install testcontainers-spicedb +``` + +You'll also need the SpiceDB Python client: + +```bash +pip install authzed +``` + +```python +from testcontainers_spicedb import SpiceDBContainer +from authzed.api.v1 import ( + InsecureClient, + WriteSchemaRequest, + CheckPermissionRequest, + CheckPermissionResponse, + ObjectReference, + SubjectReference, +) + +# Start a disposable SpiceDB instance +with SpiceDBContainer(image="authzed/spicedb:v1.47.1") as spicedb: + # Connect using the Authzed Python client (no TLS for local containers) + client = InsecureClient( + spicedb.get_endpoint(), + spicedb.get_secret_key(), + ) + + # Write a minimal schema + client.WriteSchema( + WriteSchemaRequest( + schema=""" + definition user {} + + definition document { + relation owner: user + permission read = owner + } + """ + ) + ) + + # Check a permission (e.g. for a RAG authorization gate) + response = client.CheckPermission( + CheckPermissionRequest( + resource=ObjectReference( + object_type="document", + object_id="doc1", + ), + permission="read", + subject=SubjectReference( + object=ObjectReference( + object_type="user", + object_id="alice", + ) + ), + ) + ) + + assert ( + response.permissionship + == CheckPermissionResponse.PERMISSIONSHIP_NO_PERMISSION + ) +``` + +For full test examples (relationships, consistency tokens, and post-retrieval filtering in RAG pipelines), see the complete example linked at the end. + +### Running Python Tests + +Execute tests with pytest: + +```bash +pytest test_rag_permissions.py -v +``` + +## Related Resources + +- [Full example](https://github.com/sohanmaheshwar/testcontainers-spicedb-py/blob/main/examples/full-example.py): Complete example using the SpiceDB Testcontainer +- [Using SpiceDB Go Testcontainer](https://www.docker.com/blog/rag-permission-testing-testcontainers-spicedb/) - Use a community-created Go Testcontainer module for SpiceDB diff --git a/app/spicedb/ops/_meta.ts b/app/spicedb/ops/_meta.ts index 029121c5..55e3d1dc 100644 --- a/app/spicedb/ops/_meta.ts +++ b/app/spicedb/ops/_meta.ts @@ -7,7 +7,4 @@ export default { resilience: "Improving Resilience", observability: "Observability Tooling", "load-testing": "Load Testing", - "spicedb-langchain-langgraph-rag": "Secure your RAG Pipelines using LangChain & LangGraph", - "ai-agent-authorization": "Tutorial: Authorization for AI Agents using SpiceDB", - "secure-rag-pipelines": "Tutorial: Securing RAG Pipelines with SpiceDB", }; diff --git a/app/spicedb/ops/spicedb-langchain-langgraph-rag/page.mdx b/app/spicedb/ops/spicedb-langchain-langgraph-rag/page.mdx deleted file mode 100644 index ede26ce2..00000000 --- a/app/spicedb/ops/spicedb-langchain-langgraph-rag/page.mdx +++ /dev/null @@ -1,381 +0,0 @@ -# Fine-Grained Authorization for RAG Applications using LangChain (or LangGraph) - -This guide explains how to enforce **fine-grained, per-document authorization** in Retrieval-Augmented Generation (RAG) pipelines using **SpiceDB**, **LangChain**, and **LangGraph**. - -It demonstrates how to plug authorization directly into an LLM workflow using a post-retrieval filter powered by SpiceDB — ensuring that **every document used by the LLM has been explicitly authorized** for the requesting user. - ---- - -## Overview - -Modern AI-assisted applications use RAG to retrieve documents and generate responses. -However, **standard RAG pipelines do not consider permissions** - meaning LLMs may hallucinate or leak information from unauthorized sources. - -This guide shows how to solve that problem using: - -- **SpiceDB** as the source of truth for authorization -- [**spicedb-rag-authorization**](https://github.com/sohanmaheshwar/spicedb-rag-authorization) for fast post-retrieval filtering -- **LangChain** for LLM pipelines (or) -- **LangGraph** for stateful, multi-step workflows and agents - -The library implements **post-filter authorization**, meaning: - -1. Retrieve the best semantic matches. -2. Filter them using SpiceDB permission checks. -3. Feed _only authorized documents_ to the LLM. - ---- - -## 1. Prerequisites - -### Run SpiceDB - -To run locally, use: - -```bash -docker run --rm -p 50051:50051 authzed/spicedb serve --grpc-preshared-key "sometoken" -``` - ---- - -## 2. Installation - -The package is not yet published on PyPI. -Install directly from GitHub: - -```bash -pip install "git+https://github.com/sohanmaheshwar/spicedb-rag-authorization.git#egg=spicedb-rag-auth[all]" -``` - -Or clone locally with `git clone https://github.com/sohanmaheshwar/spicedb-rag-authorization.git` and then run: - -```python -import sys -sys.path.append("/path/to/spicedb-rag-authorization") -``` - -### Create a SpiceDB schema - -We will use the [zed](https://github.com/authzed/zed) CLI to write schema and relationships. -In your production application, this would be replaced with an API call. - -```bash -zed context set local localhost:50051 sometoken --insecure -zed schema write --insecure <(cat << EOF -definition user {} -definition article { - relation viewer: user - permission view = viewer -} -EOF -) -``` - -### Add relationships - -```bash -zed relationship create article:doc1 viewer user:alice --insecure -zed relationship create article:doc2 viewer user:bob --insecure -zed relationship create article:doc4 viewer user:alice --insecure -``` - ---- - -## 3. Document Metadata Requirements - -Every document used in RAG **must include a resource ID** in metadata. -This is what enables SpiceDB to check which `user` has what permissions for each `doc`. - -```python -Document( - page_content="Example text", - metadata={"article_id": "doc4"} -) -``` - -The metadata key must match the configured `resource_id_key` which in this case is `article_id`. - ---- - -## 4. LangChain Integration - -This is the simplest way to add authorization to a LangChain RAG pipeline. - -[LangChain](https://www.langchain.com/langchain) is a framework for building LLM-powered applications by composing modular components such as retrievers, prompts, memory, tools, and models. -It provides a high-level abstraction called the LangChain Expression Language (LCEL) which lets you construct RAG pipelines as reusable, declarative graphs — without needing to manually orchestrate each step. - -You would typically use LangChain when: - -- You want a composable pipeline that chains together retrieval, prompting, model calls, and post-processing. -- You are building a RAG system where each step (retriever → filter → LLM → parser) should be easily testable and swappable. -- You need integrations with many LLM providers, vector stores, retrievers, and tools. -- You want built-in support for streaming, parallelism, or structured output. - -LangChain is an excellent fit for straightforward RAG pipelines where the control flow is mostly linear. -For more complex, branching, stateful, or agent-style workflows, you would likely [choose LangGraph](#5-langgraph-integration) instead. - -**Core component:** `SpiceDBAuthFilter` or `SpiceDBAuthLambda`. - -### Example Pipeline - -```python -auth = SpiceDBAuthFilter( - spicedb_endpoint="localhost:50051", - spicedb_token="sometoken", - resource_type="article", - resource_id_key="article_id", -) -``` - -Build your chain once: - -```python -chain = ( - RunnableParallel({ - "context": retriever | auth, # Authorization happens here - "question": RunnablePassthrough(), - }) - | prompt - | llm - | StrOutputParser() -) -``` - -Invoke: - -```python -# Pass user at runtime - reuse same chain for different users -answer = await chain.ainvoke( - "Your question?", - config={"configurable": {"subject_id": "alice"}} -) - -# Different user, same chain -answer = await chain.ainvoke( - "Another question?", - config={"configurable": {"subject_id": "bob"}} -) -``` - ---- - -## 5. LangGraph Integration - -[LangGraph](https://www.langchain.com/langgraph) is a framework for building stateful, multi-step, and branching LLM applications using a graph-based architecture. -Unlike LangChain’s linear pipelines, LangGraph allows you to define explicit nodes, edges, loops, and conditional branches — enabling **deterministic**, reproducible, agent-like workflows. - -You would choose LangGraph when: - -- You are building multi-step RAG pipelines (retrieve → authorize → rerank → generate → reflect). -- Your application needs state management across steps (conversation history, retrieved docs, user preferences). -- You require a strong separation of responsibilities (e.g., retriever node, authorization node, generator node). - -LangGraph is ideal for more advanced AI systems, such as conversational RAG assistants, agents with tool-use, or pipelines with complex authorization or business logic. - -Our [library](https://github.com/sohanmaheshwar/spicedb-rag-authorization) provides: - -- `RAGAuthState` — a TypedDict defining the required state fields -- `create_auth_node()` — auto-configured authorization node -- `AuthorizationNode` — reusable class-based node - ---- - -## 5.1 LangGraph Example - -```python -from langgraph.graph import StateGraph, END -from spicedb_rag_auth import create_auth_node, RAGAuthState -from langchain_openai import ChatOpenAI -from langchain_core.prompts import ChatPromptTemplate - -# Use the provided RAGAuthState TypedDict -graph = StateGraph(RAGAuthState) - -# Define your nodes -def retrieve_node(state): - """Retrieve documents from vector store""" - docs = retriever.invoke(state["question"]) - return {"retrieved_documents": docs} - -def generate_node(state): - """Generate answer from authorized documents""" - # Create prompt - prompt = ChatPromptTemplate.from_messages([ - ("system", "Answer based only on the provided context."), - ("human", "Question: {question}\n\nContext:\n{context}") - ]) - - # Format context from authorized documents - context = "\n\n".join([doc.page_content for doc in state["authorized_documents"]]) - - # Generate answer - llm = ChatOpenAI(model="gpt-4o-mini") - messages = prompt.format_messages(question=state["question"], context=context) - answer = llm.invoke(messages) - - return {"answer": answer.content} - -# Add nodes -graph.add_node("retrieve", retrieve_node) -graph.add_node("authorize", create_auth_node( - spicedb_endpoint="localhost:50051", - spicedb_token="sometoken", - resource_type="article", - resource_id_key="article_id", -)) -graph.add_node("generate", generate_node) - -# Wire it up -graph.set_entry_point("retrieve") -graph.add_edge("retrieve", "authorize") -graph.add_edge("authorize", "generate") -graph.add_edge("generate", END) - -# Compile and run -app = graph.compile() -result = await app.ainvoke({ - "question": "What is SpiceDB?", - "subject_id": "alice", -}) - -print(result["answer"]) # The actual answer to the question -``` - ---- - -## 5.2 Extending State with LangGraph - -Add custom fields to track additional state like conversation history, user preferences, or metadata. - -```python -class MyCustomState(RAGAuthState): - user_preferences: dict - conversation_history: list - -graph = StateGraph(MyCustomState) -# ... add nodes and edges -``` - -**When to use:** - -- Multi-turn conversations that need history -- Personalized responses based on user preferences -- Complex workflows requiring additional context - -**Example use case:** A chatbot that remembers previous questions and tailors responses based on user role (engineer vs manager). - ---- - -## 5.3 Reusable Class-Based Authorization Node - -Create reusable authorization node instances that can be shared across multiple graphs or configured with custom state key mappings. - -```python -from spicedb_rag_auth import AuthorizationNode - -auth_node = AuthorizationNode( - spicedb_endpoint="localhost:50051", - spicedb_token="sometoken", - resource_type="article", - resource_id_key="article_id", -) - -graph = StateGraph(RAGAuthState) -graph.add_node("authorize", auth_node) -``` - -You can define it once and reuse everywhere. - -```python -article_auth = AuthorizationNode(resource_type="article", ...) -video_auth = AuthorizationNode(resource_type="video", ...) - -# Use in multiple graphs -blog_graph.add_node("auth", article_auth) -media_graph.add_node("auth", video_auth) -learning_graph.add_node("auth_articles", article_auth) -``` - -**When to use:** - -- Multiple graphs need the same authorization logic -- Your state uses different key names than the defaults -- Building testable code (easy to swap prod/test instances) -- Team collaboration (security team provides authZ nodes) - -**Example use case:** A multi-resource platform (articles, videos, code snippets) where each resource type has its own authorization node that's reused across different workflows. - -For production applications, you'll often use a mix of Option 2 and 3: A custom state for your workflow + reusable authZ nodes for flexibility. -Here's an example: - -```python -class CustomerSupportState(RAGAuthState): - conversation_history: list - customer_tier: str - sentiment_score: float - -docs_auth = AuthorizationNode(resource_type="support_doc", ...) -kb_auth = AuthorizationNode(resource_type="knowledge_base", ...) - -graph = StateGraph(CustomerSupportState) -graph.add_node("auth_docs", docs_auth) -graph.add_node("auth_kb", kb_auth) -``` - ---- - -## 6. Metrics & Observability - -The library exposes: - -- number of retrieved documents -- number authorized -- denied resource IDs -- latency per SpiceDB check - -### In LangChain - -```python -auth = SpiceDBAuthFilter(..., subject_id="alice", return_metrics=True) -result = await auth.ainvoke(docs) - -print(result.authorized_documents) -print(result.total_authorized) -print(result.check_latency_ms) -# ... all other metrics -``` - -### In LangGraph - -Metrics appear in `auth_results` in the graph state. - -```python -graph = StateGraph(RAGAuthState) -# ... add nodes including create_auth_node() - -result = await app.ainvoke({"question": "...", "subject_id": "alice"}) - -# Access metrics from state -print(result["auth_results"]["total_retrieved"]) -print(result["auth_results"]["total_authorized"]) -print(result["auth_results"]["authorization_rate"]) -print(result["auth_results"]["denied_resource_ids"]) -print(result["auth_results"]["check_latency_ms"]) -``` - ---- - -## 7. Complete Example - -See the full example in the [repo here](https://github.com/sohanmaheshwar/spicedb-rag-authorization) - -- `langchain_example.py` -- `README_langchain.md` - ---- - -## 8. Next Steps - -- Read [this guide](https://authzed.com/blog/building-a-multi-tenant-rag-with-fine-grain-authorization-using-motia-and-spicedb) on creating a production-grade RAG with SpiceDB & Motia.dev -- Check out this [self-guided workshop](https://github.com/authzed/workshops/tree/main/secure-rag-pipelines) for a closer look at how fine-grained authorization with SpiceDB works in RAG. - This guide also includes the pre-filtration technique. diff --git a/app/spicedb/tutorials/_meta.ts b/app/spicedb/tutorials/_meta.ts new file mode 100644 index 00000000..0db23991 --- /dev/null +++ b/app/spicedb/tutorials/_meta.ts @@ -0,0 +1,5 @@ +export default { + "ai-agent-authorization": + "Tutorial: Authorization for AI Agents using SpiceDB", + "secure-rag-pipelines": "Tutorial: Securing RAG Pipelines with SpiceDB", +}; diff --git a/app/spicedb/ops/ai-agent-authorization/page.mdx b/app/spicedb/tutorials/ai-agent-authorization/page.mdx similarity index 100% rename from app/spicedb/ops/ai-agent-authorization/page.mdx rename to app/spicedb/tutorials/ai-agent-authorization/page.mdx diff --git a/app/spicedb/ops/secure-rag-pipelines/page.mdx b/app/spicedb/tutorials/secure-rag-pipelines/page.mdx similarity index 100% rename from app/spicedb/ops/secure-rag-pipelines/page.mdx rename to app/spicedb/tutorials/secure-rag-pipelines/page.mdx diff --git a/next-env.d.ts b/next-env.d.ts index c4b7818f..0c7fad71 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,5 +1,6 @@ /// /// +/// import "./.next/dev/types/routes.d.ts"; // NOTE: This file should not be edited diff --git a/public/images/rag-post-filter.png b/public/images/rag-post-filter.png new file mode 100644 index 00000000..4b5dff2f Binary files /dev/null and b/public/images/rag-post-filter.png differ diff --git a/public/images/rag-pre-filter.png b/public/images/rag-pre-filter.png new file mode 100644 index 00000000..6e7da659 Binary files /dev/null and b/public/images/rag-pre-filter.png differ