{"id":16628,"date":"2026-04-30T00:00:00","date_gmt":"2026-04-30T07:00:00","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/ise\/?p=16628"},"modified":"2026-04-27T07:15:37","modified_gmt":"2026-04-27T14:15:37","slug":"sharepoint-doc-level-access","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/ise\/sharepoint-doc-level-access\/","title":{"rendered":"Propagating SharePoint Document Permissions to AI Search and RAG Pipelines"},"content":{"rendered":"<h2>Executive Summary<\/h2>\n<p>When integrating SharePoint content into downstream systems such as <strong>Azure AI Search<\/strong>, <strong>RAG pipelines<\/strong>, or <strong>Copilot extensions<\/strong>, preserving document-level access control is critical\u2014especially for highly sensitive content.<\/p>\n<p>This post describes a <strong>security-first architecture<\/strong> that propagates SharePoint document permissions into downstream systems so that authorization is enforced at query time. The approach:<\/p>\n<ul>\n<li>Uses Microsoft Graph&#8217;s <strong>Sites.Selected<\/strong> permission for least-privilege access to SharePoint<\/li>\n<li>Materializes document-level permissions into search index fields at ingestion time<\/li>\n<li>Relies on <strong>Microsoft Entra ID object IDs (GUIDs)<\/strong> for stable, query-time filtering<\/li>\n<\/ul>\n<h2>Introduction: The Problem<\/h2>\n<p>In our last project, the customer needed to generate tailored briefings for multiple user groups. Creating those briefings required preprocessing a large volume of documents stored in SharePoint folders\u2014many containing highly sensitive data. That meant we had to carry SharePoint&#8217;s document-level permissions all the way through downstream search and retrieval systems so only authorized users could see the right content.<\/p>\n<p>SharePoint provides rich, hierarchical, and inheritable permissions\u2014but downstream systems like Blob Storage, search indexes, and LLM retrieval layers do not natively understand SharePoint ACLs.<\/p>\n<p>Without an explicit permission-mapping strategy, organizations risk:<\/p>\n<ul>\n<li><strong>Overexposing sensitive documents<\/strong> to unauthorized users<\/li>\n<li><strong>Violating Zero Trust principles<\/strong> by granting broad access<\/li>\n<li><strong>Failing internal security or compliance reviews<\/strong><\/li>\n<\/ul>\n<p>A common anti-pattern is granting applications <strong><code>Sites.Read.All<\/code><\/strong>, which unintentionally exposes <em>all<\/em> sites in a tenant. We needed a pattern that preserves document-level authorization information when we ingest content into downstream systems.<\/p>\n<h2>The Journey: Our Approach and Solution<\/h2>\n<p>We built a security-first pipeline that reads documents and permissions from SharePoint, normalizes identities to Microsoft Entra ID object IDs, and stores both content and ACL metadata in a search index. The ingestion app uses the Microsoft Graph <code>Sites.Selected<\/code> permission; everything downstream operates on the materialized permission data.<\/p>\n<h3>Design Goals<\/h3>\n<table>\n<thead>\n<tr>\n<th>Goal<\/th>\n<th>Description<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>\u2705 Least-privilege access<\/td>\n<td>Explicit allow-listing per site\u2014no tenant-wide permissions<\/td>\n<\/tr>\n<tr>\n<td>\u2705 Document-level ACLs<\/td>\n<td>Materialize permissions so downstream systems can filter safely<\/td>\n<\/tr>\n<tr>\n<td>\u2705 Deterministic identities<\/td>\n<td>Use GUIDs instead of emails or UPNs<\/td>\n<\/tr>\n<tr>\n<td>\u2705 Broad compatibility<\/td>\n<td>Work with Copilot extensions, RAG retrievers, and search indexes<\/td>\n<\/tr>\n<tr>\n<td>\u2705 Highly sensitive content<\/td>\n<td>Safe for regulated documents and compliance scenarios<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<h3>Architecture Overview<\/h3>\n<p>The solution comprises five key components working together:<\/p>\n<table>\n<thead>\n<tr>\n<th>Component<\/th>\n<th>Role<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><strong>SharePoint Online<\/strong><\/td>\n<td>Hosts documents and source permissions<\/td>\n<\/tr>\n<tr>\n<td><strong>Microsoft Graph API<\/strong><\/td>\n<td>Supplies document metadata and ACLs<\/td>\n<\/tr>\n<tr>\n<td><strong>Sites.Selected permission<\/strong><\/td>\n<td>Ensures zero default access; each site is explicitly granted<\/td>\n<\/tr>\n<tr>\n<td><strong>Ingestion pipeline<\/strong><\/td>\n<td>Reads documents and permissions, resolves effective ACLs, and normalizes identities<\/td>\n<\/tr>\n<tr>\n<td><strong>Search index with security trimming<\/strong><\/td>\n<td>Stores <code>allowedUsers<\/code> and <code>allowedGroups<\/code> for query-time filters<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<h3>End-to-End Permission Flow<\/h3>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/ise\/wp-content\/uploads\/sites\/55\/2026\/04\/sharepoint-doc-level-access-permission-flow.webp\" alt=\"End-to-end permission flow from SharePoint to filtered search\" \/><\/p>\n<h2>The Destination: Outcomes and Learnings<\/h2>\n<p>After piloting this pattern in production workloads, here\u2019s what held up.<\/p>\n<h3>Permission Scoping with Sites.Selected<\/h3>\n<p>As described above, the ingestion application\u2014the component that reads documents and their metadata from SharePoint and writes them into the search index\u2014is registered with the <strong><code>Sites.Selected<\/code><\/strong> application permission. This means:<\/p>\n<table>\n<thead>\n<tr>\n<th>Characteristic<\/th>\n<th>Benefit<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Zero access by default<\/td>\n<td>The app cannot read any site until explicitly granted<\/td>\n<\/tr>\n<tr>\n<td>Explicit site grants<\/td>\n<td>SharePoint admins must allow each site individually<\/td>\n<\/tr>\n<tr>\n<td>Enforced by SharePoint<\/td>\n<td>Access control is platform-enforced, not app logic<\/td>\n<\/tr>\n<tr>\n<td>Clear audit trail<\/td>\n<td>Every grant is traceable and revocable<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>This sharply limits blast radius if the app is ever compromised and keeps the pattern aligned with Zero Trust principles.<\/p>\n<h3>Extract and Normalize Permissions via Microsoft Graph<\/h3>\n<p>For each document, effective permissions are retrieved using Microsoft Graph. The key challenge is that SharePoint permissions are hierarchical and inheritable, whereas downstream systems are ACL-agnostic. To bridge this gap, permissions must be <strong>resolved at ingestion time<\/strong> and stored explicitly.<\/p>\n<pre><code class=\"language-http\">GET \/sites\/{site-id}\/drive\/items\/{item-id}\/permissions<\/code><\/pre>\n<p>From the response, we extract:<\/p>\n<ul>\n<li><strong>User assignments<\/strong> \u2014 individual users with access<\/li>\n<li><strong>Group assignments<\/strong> \u2014 security groups and Microsoft 365 groups<\/li>\n<li><strong>Identity normalization<\/strong> \u2014 convert all identities to Microsoft Entra ID object IDs (GUIDs)<\/li>\n<\/ul>\n<pre><code class=\"language-python\">permissions = await graph_client.get_permissions(drive_id, item_id)\r\n\r\nallowed_users = []\r\nallowed_groups = []\r\n\r\nfor entry in permissions:\r\n    grant = entry.get(\"grantedToV2\", {})\r\n    user = grant.get(\"user\")\r\n    group = grant.get(\"group\")\r\n\r\n    if user and user.get(\"id\"):\r\n        allowed_users.append(user[\"id\"])  # Microsoft Entra ID object ID\r\n    if group and group.get(\"id\"):\r\n        allowed_groups.append(group[\"id\"])  # Group object ID<\/code><\/pre>\n<p><strong>Key decisions:<\/strong><\/p>\n<table>\n<thead>\n<tr>\n<th>Decision<\/th>\n<th>Rationale<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Use GUIDs, not emails<\/td>\n<td>Object IDs remain stable across renames and domain changes<\/td>\n<\/tr>\n<tr>\n<td>Preserve group IDs<\/td>\n<td>Enables offline expansion when search can&#8217;t expand groups natively<\/td>\n<\/tr>\n<tr>\n<td>Resolve inheritance once<\/td>\n<td>Index holds the effective ACL for each document<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<h3>Index Security Materialization<\/h3>\n<p>Permissions are stored directly in the search index as filterable fields:<\/p>\n<pre><code class=\"language-python\">chunk = {\r\n    \"content\": document_text,\r\n    \"allowedUsers\": allowed_users,\r\n    \"allowedGroups\": \",\".join(allowed_groups),\r\n}<\/code><\/pre>\n<p>At query time, results are filtered using the authenticated user&#8217;s Microsoft Entra ID object ID and group memberships:<\/p>\n<pre><code class=\"language-text\">allowedUsers\/any(u: u eq '{user_oid}') or allowedGroups\/any(g: g eq '{group_oid}')<\/code><\/pre>\n<p><strong>Benefits:<\/strong><\/p>\n<ul>\n<li>\u2705 Authorization runs <strong>before<\/strong> documents are returned to the caller<\/li>\n<li>\u2705 Same pattern works for RAG retrievers and Copilot extensions<\/li>\n<li>\u2705 No post-retrieval filtering required\u2014secure by design<\/li>\n<\/ul>\n<h3>Security Characteristics<\/h3>\n<p>This architecture delivers strong security guarantees:<\/p>\n<table>\n<thead>\n<tr>\n<th>Property<\/th>\n<th>Status<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>No tenant-wide content access<\/td>\n<td>\u2705<\/td>\n<\/tr>\n<tr>\n<td>Explicit site allow-listing<\/td>\n<td>\u2705<\/td>\n<\/tr>\n<tr>\n<td>Deterministic identity model<\/td>\n<td>\u2705<\/td>\n<\/tr>\n<tr>\n<td>Secure by default for AI search and RAG<\/td>\n<td>\u2705<\/td>\n<\/tr>\n<tr>\n<td>Compatible with Copilot extensions<\/td>\n<td>\u2705<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<h3>Limitation: Stale Permissions<\/h3>\n<p>One important trade-off of materializing permissions at ingestion time is that <strong>permission changes in SharePoint are not automatically propagated<\/strong> to downstream systems. If a user&#8217;s access is revoked in SharePoint, that user may still see the document&#8217;s content in the search index or RAG pipeline until the next ingestion run.<\/p>\n<p>To mitigate this:<\/p>\n<ul>\n<li><strong>Run the ingestion pipeline on a regular schedule<\/strong> so that permission updates are picked up in a timely manner.<\/li>\n<li><strong>Use SharePoint webhooks or event receivers<\/strong> to trigger re-ingestion when permissions change, reducing the staleness window.<\/li>\n<li><strong>Tune the refresh interval<\/strong> based on the sensitivity of the content\u2014highly regulated data may warrant more frequent re-ingestion.<\/li>\n<li><strong>Communicate the expected propagation delay<\/strong> to stakeholders so they understand the security posture.<\/li>\n<\/ul>\n<p>In our engagement, the ingestion pipeline ran on a periodic schedule, and the customer accepted a bounded delay for permission propagation given the sensitivity profile of the content.<\/p>\n<h3>Common Pitfalls to Avoid<\/h3>\n<table>\n<thead>\n<tr>\n<th>Pitfall<\/th>\n<th>Why It&#8217;s Dangerous<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>\u274c Using <code>Sites.Read.All<\/code><\/td>\n<td>Breaks least privilege\u2014exposes entire tenant<\/td>\n<\/tr>\n<tr>\n<td>\u274c Filtering results after retrieval<\/td>\n<td>Data already leaked to the application layer<\/td>\n<\/tr>\n<tr>\n<td>\u274c Relying on emails or display names<\/td>\n<td>These change; GUIDs don&#8217;t<\/td>\n<\/tr>\n<tr>\n<td>\u274c Ignoring group expansion<\/td>\n<td>Unexpanded groups lead to silent overexposure<\/td>\n<\/tr>\n<tr>\n<td>\u274c Assuming real-time permission sync<\/td>\n<td>Materialized permissions can become stale\u2014plan for periodic refresh<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<h2>Conclusion<\/h2>\n<p>Our customer needed to expose SharePoint content through AI-powered search and Copilot integrations without compromising document-level access control. By materializing permissions at ingestion time and filtering at query time, we delivered a solution that preserved the customer&#8217;s existing SharePoint security model across every downstream system.<\/p>\n<p>When SharePoint content is integrated into AI-powered systems, <strong>authorization becomes a data problem<\/strong>. Treating permissions as first-class data ensures the system remains secure, auditable, and future-proof.<\/p>\n<p><strong>Key takeaways:<\/strong><\/p>\n<ul>\n<li>Use <strong>Sites.Selected<\/strong> for least-privilege access when the ingestion application reads from SharePoint<\/li>\n<li>Document-level permissions must be <strong>materialized explicitly<\/strong> in your downstream index<\/li>\n<li>GUID-based filtering ensures stable, deterministic identity matching<\/li>\n<li>Authorization should always happen <strong>before retrieval<\/strong>, not after<\/li>\n<li>Plan for <strong>periodic re-ingestion<\/strong> to keep materialized permissions in sync with SharePoint<\/li>\n<\/ul>\n<p>This pattern has proven effective for <strong>enterprise-grade search, RAG pipelines, and Copilot integrations<\/strong> operating over highly sensitive documents.<\/p>\n<h2>Further Reading<\/h2>\n<ul>\n<li><a href=\"https:\/\/learn.microsoft.com\/en-us\/graph\/api\/driveitem-list-permissions\">Microsoft Graph API: List item permissions<\/a><\/li>\n<li><a href=\"https:\/\/learn.microsoft.com\/en-us\/graph\/permissions-reference#sitesselected\">Microsoft Graph: Sites.Selected permission<\/a><\/li>\n<li><a href=\"https:\/\/learn.microsoft.com\/en-us\/azure\/search\/search-security-trimming-for-azure-search-with-aad\">Azure AI Search: Security trimming with Microsoft Entra ID<\/a><\/li>\n<li><a href=\"https:\/\/learn.microsoft.com\/en-us\/sharepoint\/dev\/apis\/webhooks\/overview-sharepoint-webhooks\">SharePoint webhooks overview<\/a><\/li>\n<li><a href=\"https:\/\/learn.microsoft.com\/en-us\/azure\/search\/search-security-rbac\">Azure AI Search: Document-level access control (preview)<\/a><\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>A security-first architecture that propagates SharePoint document permissions into downstream AI systems, using Microsoft Entra ID object IDs (GUIDs) for safe, query-time filtering in Azure AI Search, RAG pipelines, and Copilot extensions.<\/p>\n","protected":false},"author":210891,"featured_media":16629,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[1,3451],"tags":[3649,3647,3646,3553,3648,3645],"class_list":["post-16628","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-cse","category-ise","tag-copilot","tag-microsoft-entra-id","tag-microsoft-graph","tag-rag","tag-search","tag-sharepoint"],"acf":[],"blog_post_summary":"<p>A security-first architecture that propagates SharePoint document permissions into downstream AI systems, using Microsoft Entra ID object IDs (GUIDs) for safe, query-time filtering in Azure AI Search, RAG pipelines, and Copilot extensions.<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/posts\/16628","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/users\/210891"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/comments?post=16628"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/posts\/16628\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/media\/16629"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/media?parent=16628"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/categories?post=16628"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/tags?post=16628"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}