{"slug": "rag-access-control-3-ways-to-enforce-document-permissions-in-ai-apps", "title": "RAG access control: 3 ways to enforce document permissions in AI apps", "summary": "Databricks and AWS published opposing approaches to RAG access control on October 7, 2026, with Databricks making on-behalf-of-user (OBO) authorization generally available for Databricks Apps and AWS arguing that copied permissions go stale so retrieval should filter on cached ACLs and then re-verify each candidate against the source system at query time. Databricks forwards the signed-in user's token in the x-forwarded-access-token HTTP header so Unity Catalog enforces that user's existing permissions, including row filters and column masks, while AWS targets content living in SharePoint, Google Drive, and Confluence. A third model applies when an organization owns its content but the corpus is too large to authorize one chunk at a time.", "body_md": "# RAG access control: 3 ways to enforce document permissions in AI apps\n\nOn-behalf-of tokens (Databricks), live ACL re-checks (AWS), and parent-level fine-grained authorization: how each keeps an AI assistant from showing users data they can't access, and when to use which.\n\nAn AI feature that reads company data has to answer a question before it answers the user's: which rows, documents, and fields is this particular person allowed to see? Get that wrong once and an assistant cheerfully summarizes the compensation sheet for someone who was never meant to find it.\n\nOn October 7, 2026, Databricks and AWS published answers that point in opposite directions. [Databricks made on-behalf-of-user (OBO) authorization generally available](https://www.databricks.com/blog/now-ga-building-permission-aware-databricks-apps-behalf-user-authorization) for Databricks Apps: the signed-in user's token is forwarded on each request, and Unity Catalog enforces that user's existing permissions, including row filters and column masks. [AWS came at the same problem from the other side](https://aws.amazon.com/blogs/machine-learning/rethinking-access-control-for-rag-with-amazon-quick-and-amazon-bedrock/). Its argument is that copied permissions go stale, so retrieval should filter on cached ACLs first and then verify each surviving candidate against the source system at query time.\n\nNeither is wrong. The constraint that decides which one you get is simple: can the system holding your data answer \"may this user see this?\" on the request path, cheaply, every time? For Databricks that holds, because it owns both the data and the policy engine. For the AWS scenario it does not, because the content lives in SharePoint, Google Drive, and Confluence. A third model covers the case where you own the content but the corpus is too large to authorize one chunk at a time.\n\n## Model 1: Forward the user's token on every request\n\nIn the Databricks model, the app never decides anything about permissions. Databricks forwards the current user's access token in the `x-forwarded-access-token` HTTP header, and the app passes that token to the SQL connector for the request that needs user context. The query then runs as the user, and Unity Catalog applies that user's row filters and column masks. The app never reimplements data governance rules in its own code.\n\nAdapted from the [Databricks Apps authorization docs](https://docs.databricks.com/aws/en/dev-tools/databricks-apps/auth), with the missing-token check pulled into a helper the way the Databricks post recommends:\n\nIf the OBO pattern is new to you, [OAuth's On-Behalf-Of flow for AI agents](https://workos.com/blog/oauth-on-behalf-of-ai-agents) covers the protocol side. Two things keep this narrow. The token is short-lived and per request: the app reads it on each request and never stores it across requests or in a session. And API scopes cap what the app may do with the identity it borrowed. A read-only analytics assistant should request `sql:restricted-query` rather than the broader `sql` scope, because `sql:restricted-query` allows only read-only SQL queries. Scopes are a capability ceiling, not a grant of data access: the app needs the scope, and the user still needs permission on the target resource. Workspace administrators cap which scopes developers may request at all, and can set that allowlist to None to turn user authorization off entirely.\n\nStaleness isn't really a concept here, because nothing was copied. In Databricks' words, when administrators update Unity Catalog policies, subsequent app requests reflect those changes. The cost is the precondition: your data has to live somewhere that can authenticate the end user and evaluate its own policy on every call. That describes a warehouse. It does not describe a folder tree in a third-party SaaS product reached through a sync connector.\n\n## Model 2: Filter on cached ACLs, then re-check the source\n\nAWS starts from the pattern most teams build first. A connector pulls ACLs during a periodic sync, the ACLs land in the index as attributes, and at query time the system maps the signed-in user onto those attributes and filters results. AWS's objection is structural: in that model, the AI system \"takes sole responsibility for enforcement without being the authoritative source of permissions.\"\n\nThree failure modes follow. Connectors have to replicate complex, source-specific ACL logic, and mapping inheritance hierarchies, group memberships, conditional access policies, and deny rules across dozens of connectors is error-prone. The copy is a snapshot from the last sync, so between syncs a user whose access was revoked may still get answers from documents they should no longer see. Event-driven updates only patch part of this, because not every source emits the right events: AWS notes that Confluence doesn't emit one when group membership changes. And sources keep changing, so a new SharePoint permission feature or a shift in the Google Drive sharing model can open a gap until the connector catches up.\n\nAWS's answer, described for Amazon Quick and Amazon Bedrock Knowledge Bases, is a real-time ACL check layered on top of the existing pre-retrieval filtering. Stage one runs semantic search against the vector index with the stored ACLs applied, producing a candidate set. AWS keeps that stage because real-time API calls for every document in the index would be too costly at scale. Stage two then calls the source. In the post's Google Drive walkthrough, an Amazon Quick agent verifies each candidate against the Drive APIs, using the administrator-provided service account credential to generate user-specific tokens through impersonation, drops the documents the user can't read, and passes only verified passages to the model. AWS says a revocation is then reflected in AI responses \"within moments, not hours or days.\"\n\nYou pay for that with live API calls on the hot path, bounded by the candidate set rather than the corpus. And it only works when the source can answer a per-user permission question. Google Drive can. An internal store with one shared service credential and no impersonation can't, and stage two has nothing to call.\n\n## Model 3: Authorize stable parents and filter on parent IDs\n\nThe third model applies when you own the content and the problem is cardinality, not provenance. RAG systems generate high-cardinality resources by design: one PDF becomes dozens of chunks, a documentation site becomes thousands of embeddings, an enterprise knowledge base becomes millions of vectors. Register every one of those in an authorization service and every ingestion becomes an auth write, every deletion becomes a cleanup task to avoid orphaned permissions, and drift between the vector store and the auth system becomes inevitable.\n\nAccess usually follows a hierarchy anyway. People are granted collections, workspaces, projects, or folders, and documents inherit from those. The [WorkOS FGA docs](https://workos.com/docs/fga/resources) put the rule of thumb plainly: millions of objects \"should stay in your database, with a reference to the nearest FGA-managed parent.\" So register the stable parents as resources, assign roles at the parent level, tag each vector with its parent ID as metadata, ask for the parents a user can access in one call, and let the vector database do the filtering locally. We walk through the cost of the alternative, syncing every document into the authorization system, in [Authorization for RAG at scale: why you shouldn't sync every document](https://workos.com/blog/authorization-for-rag-at-scale-why-you-shouldnt-sync-every-document).\n\nBecause [permissions flow down the hierarchy](https://workos.com/docs/fga/model-your-app-multi-level-inheritance), a member with a role on the whole knowledge base sees every collection in it, while a member granted a single collection sees only that one, with no change to the search code. When a specific document needs its own grant, register that document as a resource and keep the rest of the corpus hierarchical, so per-document authorization stays the exception.\n\nOnly the parent ID gets copied onto each vector, and it changes only when content moves between collections. The permissions themselves are evaluated at query time.\n\n## How the three compare\n\n## Fail closed, and never fall back to the service principal\n\nDatabricks puts the rule every one of these models needs in two sentences:\n\nA missing-token error is loud and easy to catch. A plausible answer generated under the app's own identity is silent, looks exactly like a correct one, and nothing downstream can tell the difference.\n\nThe same reflex belongs in the other two models, even though neither source spells out the failure case. If the live source check in stage two errors, drop the candidate rather than trusting the cached ACL that stage one matched on, because that cached ACL is the thing stage two exists to distrust. If the call that returns the accessible parents fails, return no results rather than running the vector search without a filter. And make sure an empty list stays an empty filter: code that skips the filter when there is nothing in it turns \"no access\" into \"full access.\"\n\nDelegation should only ever narrow. In the Databricks model, the scope limits what the app can do and the user's Unity Catalog permissions limit what data it can reach; both have to allow the request. Falling back to the app's identity removes the user's half of that check, which is the one failure you can't see from the output.\n\n## Most apps run two of these at once\n\nPicking a model isn't picking one for the whole application. Databricks supports both app authorization, which uses the app's dedicated service principal, and user authorization, which uses the signed-in user's identity, and says most production apps can use both, choosing the appropriate identity for each request path. Shared configuration, background jobs, and application metrics belong to the app. Anything whose answer should differ per viewer belongs to the user.\n\nThe wiring matters more than the taxonomy. Databricks recommends keeping app-owned and user-authorized clients separate in code, tests, and dependency wiring, rather than building one generic client and reusing it everywhere. For agents deployed on Databricks Apps, it says to initialize the user-scoped client inside the request-time invoke or stream handler rather than at startup, because the forwarded token only exists during an active user request. A single shared client is how a service-principal credential ends up serving a user-specific path, and that mistake does not throw an exception. It returns data.\n\n## Which model fits\n\n**Forward the user's token** when the data already lives behind a policy engine that knows your users. You get personalization and governance from the same mechanism, with no second system to keep in sync.\n\n**Re-check at query time** when the content belongs to someone else's SaaS and that SaaS can answer per-user permission questions. You are paying latency to avoid owning a permission model you can't keep accurate.\n\n**Authorize parents and filter locally** when you own the content and the chunk count dwarfs the number of real access decisions. One call against a small, stable hierarchy beats millions of writes against a large, churning one.\n\nThen write down, per request path, whose identity runs the query, and make that boundary visible in the code. Picking the wrong model rarely burns you. A path that was supposed to run as the user and quietly ran as the app will.", "url": "https://wpnews.pro/news/rag-access-control-3-ways-to-enforce-document-permissions-in-ai-apps", "canonical_source": "https://workos.com/blog/rag-access-control-document-permissions", "published_at": "2026-10-12 00:00:00+00:00", "updated_at": "2026-10-12 15:50:43.752905+00:00", "lang": "en", "topics": ["artificial-intelligence", "ai-agents", "ai-infrastructure", "mlops", "ai-safety"], "entities": ["Databricks", "AWS", "Unity Catalog", "Databricks Apps", "Amazon Bedrock", "Amazon Quick", "SharePoint", "Google Drive"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/rag-access-control-3-ways-to-enforce-document-permissions-in-ai-apps", "markdown": "https://wpnews.pro/news/rag-access-control-3-ways-to-enforce-document-permissions-in-ai-apps.md", "text": "https://wpnews.pro/news/rag-access-control-3-ways-to-enforce-document-permissions-in-ai-apps.txt", "jsonld": "https://wpnews.pro/news/rag-access-control-3-ways-to-enforce-document-permissions-in-ai-apps.jsonld"}}