Security roles get users to tables. Row-level filtering inside those tables is what turns "all customers" into "customers in my country" or "transactions for my legal entity". Organizations with geographic salesforces, multi-legal-entity consolidations, or segregation-of-duties requirements run into this the moment they try to make Customer-360 work.
Extensible Data Security (XDS) is the F&O mechanism for this. It has four moving parts, and when any one is wrong the policy silently returns all rows - the worst failure mode because nobody notices until an audit.
What an XDS policy actually does
An XDS policy attaches a filter to a constrained table based on a primary table and a context clause. When a user in a specific context queries the constrained table, F&O appends an implicit WHERE clause derived from the primary table lookup.
The four pieces:
- Primary table + view - the source of the user-specific context (e.g., SalesEmployee linked to the current user)
- Constrained table + view - the target that needs filtering (e.g., CustTable)
- Policy query - the join between primary and constrained, with any additional filters
- Context clause - when the policy applies (role context, application context, or always)
When all four align, filtering works. When any is misconfigured, the policy quietly does nothing.
Three configuration traps
Context clause too broad
Setting Context Type to ContextString with a string no role actually sets means the policy exists but never activates. The most common outcome: a policy that looks right on paper but never appends its WHERE clause.
Fix: use RoleName context bound to the specific security role the filter should apply to. Don't use arbitrary context strings unless you're wiring them from code.
Policy query missing DataAreaId join
A policy joining SalesEmployee to CustTable often works in the user's default company and silently fails in other legal entities. The cross-company context doesn't add dataAreaId implicitly - the policy query needs it explicitly.
Every XDS policy query touching company-specific tables needs an explicit dataAreaId match, even when both tables are SaveDataPerCompany.
Constrained tables missing access paths
A policy constrains CustTable but users still see customer data via CustTransOpen on a collection report - the report queries CustTransOpen directly without going through CustTable, bypassing the filter.
Constrained tables must include every table in the access path you want to block. Building a coverage matrix from actual data-access patterns (forms, reports, queries) catches the ones developers miss.
Debugging a policy that doesn't fire
Work from the bottom of the stack up:
- Inspect query SQL via SQL Server Management Studio or Trace Parser. Run the form as the test user, capture the SQL, look for the expected WHERE clause. If missing, the policy isn't being appended.
- Check policy enabled via Security and Compliance Studio or SecurityPolicyTable. Policies can be disabled at runtime by configuration.
- Check role context - what role does the test user hold? Does the policy reference that exact role name?
- Check model deployment - policies live in a model; the model must be deployed to the environment for the policy to exist there.
Performance cost
XDS adds an implicit JOIN to every query against the constrained table. The query optimizer usually handles this well. Complex policies with multi-hop joins can degrade measurably, especially on large constrained tables.
Patterns that keep XDS fast:
- Index the filter columns in both primary and constrained tables. The implicit JOIN needs support.
- Minimize the primary view scope - return only the columns the policy uses.
- Test with representative data - a policy fine on 100 rows can fall over at 500k.
- Profile via Trace Parser when a common form slows after policy enablement.
Primary table relationships
The relationship between primary and constrained tables drives the query plan. Keep it simple: one primary table, direct join to the constrained table, explicit dataAreaId, explicit user binding. Multi-hop primary chains produce query plans the optimizer guesses at, and guesses are what go wrong at scale.
A workable multi-country pattern
Organizations running salespeople in multiple countries on a shared F&O tenant typically ship:
- Primary table: SalesEmployee joined to UserRelations for the current-user binding
- Constrained tables: CustTable, CustInvoiceTable, CustTrans, SalesTable, SalesLine (and every report-specific table that touches customer data)
- Context: RoleName bound to a country-specific sales role
- Query: explicit dataAreaId match, country filter from the primary view
- Perf: indexes on SalesEmployee.UserID and all constrained-table primary keys
The work is one-off. The audit compliance it produces lasts years.
When the policy is working
A correctly configured XDS is invisible. Users don't see rows they shouldn't. Reports filter consistently across every entry point. The audit log shows filtered queries, not full-table scans. Nothing visually breaks. That invisibility is why the configuration discipline matters - when something is wrong, nobody notices.