Wednesday, April 15, 2026

Implementing Fine-Grained Row-Level Access Controls (RLAC) in Vector Search

Retrieval-Augmented Generation (RAG) unlocks powerful Enterprise search over unstructured document repositories. However, standard RAG implementations often face security challenges. In thousands of organisations, a security gap exists.  Once documents are ingested into a unified vector store, vector similarity search operates indiscriminately across security boundaries. An employee querying human resources policies might inadvertently retrieve vector chunks originating from executive compensation plans or restricted financial forecasts.

To make RAG enterprise-ready, identity aware document retrieval is mandatory. Security filters must execute directly within the retrieval query engine, ensuring users only retrieve chunks they are authorised to access under Active Directory / Entra ID policies.

This guide explores how to build fine-grained, row-level access control (RLAC) into Azure AI Search vector indexes connected to Microsoft Azure AI Foundry workloads using Python and TypeScript.

The Security Gap in Standard Vector Retrieval
In naive RAG deployments, document chunking and vector indexing strip away access control lists (ACLs). 

Consider an index containing the below for example:

  • Public HR policy documents (Group: Everyone)
  • Regional manager operational guidelines (Group: Regional_Managers)
  • Sensitive salary metadata (Group: Compensation_HR)

When a user executes a hybrid or dense vector query via Microsoft Azure AI Foundry SDK, the cosine similarity step evaluates distance against every vector in the index. The LLM then receives retrieved context containing chunks the user lacks permissions to view. Filtering post retrieval (in memory) is insufficient because it reduces the effective k nearest neighbors returned to the LLM, causing context deprivation and potential security leaks.

Architecture: Identity Aware Filter Ingestion
To implement native RLAC in Azure AI Search, security metadata must be indexed alongside the payload and vector embeddings.



During indexing, capture Microsoft Entra ID object identifiers (OIDs) or security group IDs permitted to read the document.

Index Schema Definition (Python)

  
from azure.core.credentials import AzureKeyCredential
from azure.search.documents.indexes import SearchIndexClient
from azure.search.documents.indexes.models import (
    SearchIndex,
    SimpleField,
    SearchableField,
    SearchField,
    SearchFieldDataType,
    VectorSearch,
    HnswAlgorithmConfiguration,
    VectorSearchProfile
)


def create_rlac_vector_index(endpoint: str, credential: AzureKeyCredential, index_name: str):
    client = SearchIndexClient(endpoint=endpoint, credential=credential)
    
    fields = [
        SimpleField(name="id", type=SearchFieldDataType.String, key=True),
        SearchableField(name="content", type=SearchFieldDataType.String),
        SearchField(
            name="content_vector",
            type=SearchFieldDataType.Collection(SearchFieldDataType.Single),
            searchable=True,
            vector_search_dimensions=1536,
            vector_search_profile_name="myHnswProfile"
        ),
        # Security Filter Attributes
        SimpleField(
            name="allowed_groups",
            type=SearchFieldDataType.Collection(SearchFieldDataType.String),
            filterable=True
        ),
        SimpleField(
            name="owner_oid",
            type=SearchFieldDataType.String,
            filterable=True
        )
    ]
    
    vector_search = VectorSearch(
        algorithms=[HnswAlgorithmConfiguration(name="myHnsw")],
        profiles=[VectorSearchProfile(name="myHnswProfile", algorithm_configuration_name="myHnsw")]
    )
    
    index = SearchIndex(name=index_name, fields=fields, vector_search=vector_search)
    client.create_or_update_index(index)



  

Security Filtered Vector Retrieval Pipeline

When a client queries Azure AI Foundry, extract their Entra ID JWT payload to read their oid and groups claims. Construct an Azure AI Search OData filter expression to apply alongside the vector query.



Python
import os from azure.identity import DefaultAzureCredential from azure.search.documents import SearchClient from azure.search.documents.models import VectorizedQuery from azure.ai.evaluation import AzureAIProjectClient def query_vector_store_with_rlac( search_endpoint: str, index_name: str, query_text: str, query_vector: list[float], user_oid: str, user_groups: list[str] ): credential = DefaultAzureCredential() search_client = SearchClient(endpoint=search_endpoint, index_name=index_name, credential=credential) # Format OData security filter expression groups_filter = ", ".join([f"'{g}'" for g in user_groups]) odata_filter = f"owner_oid eq '{user_oid}' or allowed_groups/any(g: search.in(g, '{groups_filter}'))" vector_query = VectorizedQuery( vector=query_vector, k_nearest_neighbors=5, fields="content_vector" ) results = search_client.search( search_text=query_text, vector_queries=[vector_query], filter=odata_filter, select=["id", "content"] ) retrieved_chunks = [doc["content"] for doc in results] return retrieved_chunks


Performance & Optimisation Best Practices

Index Warmup & Memory Overhead: Adding string collection fields for security filtering increases index memory overhead slightly. Ensure filterable attributes are explicitly flagged with filterable=True and avoid making large security collections searchable.

Token Invalidation: When user permissions change in Microsoft Entra ID, ensure token caches expire appropriately to reflect revoked security groups.



No comments:

Implementing Fine-Grained Row-Level Access Controls (RLAC) in Vector Search

Retrieval-Augmented Generation (RAG) unlocks powerful Enterprise search over unstructured document repositories. However, standard RAG imple...