16.2 Extensible Data Security (XDS) Policies
Key Takeaways
- Extensible Data Security (XDS) provides declarative row-level and record-level security by dynamically injecting SQL EXISTS joins and subqueries into database queries generated by the AOS runtime.
- A security policy in the AOT consists of a Primary Table, a Policy Query defining filtering criteria, Constrained Tables filtered by the policy, and a Context Type governing policy activation.
- Context Type options include RoleName (applies to explicit roles), RoleProperty (matches role ContextString properties), and ContextString (activated programmatically at runtime using XDS::SetContext()).
- Developers must avoid outer joins in policy queries and ensure composite index alignment on foreign key relations between constrained and query tables to prevent debilitating table scans.
- System Administrators bypass XDS policies unconditionally, and tables with CacheLookup set to EntireTable must never be constrained by dynamic XDS policies.
16.2 Extensible Data Security (XDS) Policies
Quick Answer: Extensible Data Security (XDS) is the declarative, kernel-level framework in Dynamics 365 Finance and Operations that enforces record-level (row-level) security. Unlike role-based security, which controls access to entire forms, menus, and tables, XDS restricts which specific rows a user can view, edit, or delete within a table. Created under AOT > Security > Security Policies, an XDS policy pairs a Primary Table and an AOT Policy Query with one or more Constrained Tables. The AOS runtime dynamically appends the policy query as an
EXISTS JOINor subquery to every SQL statement executed against constrained tables. Policies are activated based on their Context Type:RoleName,RoleProperty, or programmatically viaContextStringusingXDS::SetContext(). System Administrators bypass all XDS policies unconditionally.
1. The Extensible Data Security (XDS) Framework
While Role-Based Security determines whether a user can access a menu item or update records in a table, business compliance frequently mandates restricting data by record attributes (e.g., A sales manager should only see customers belonging to their assigned sales region, or A warehouse clerk should only see inventory in their local warehouse).
Historically, legacy systems utilized Record Level Security (RLS), which relied on raw SQL WHERE clause string manipulation that suffered from poor performance, cache invalidation, and maintenance overhead. XDS completely replaced RLS in modern Dynamics 365 Finance and Operations:
Role-Based Security vs. Extensible Data Security (XDS)
┌─────────────────────────────────────────┐ ┌─────────────────────────────────────────┐
│ Role-Based Security │ │ Extensible Data Security (XDS) │
│ • Object-level authorization │ │ • Record-level / Row-level filtering │
│ • Governs forms, menus, tables, fields │ │ • Governs specific data rows in tables │
│ • Can the user open "All Customers"? │ │ • Which specific customers appear? │
│ • Evaluated during navigation / load │ │ • Injected into every SQL query by AOS │
└─────────────────────────────────────────┘ └─────────────────────────────────────────┘
Kernel-Level SQL Rewriting
XDS operates inside the Application Object Server (AOS) data access layer. Whenever an affected user issues a query against a constrained table (whether through a form, report, OData endpoint, data entity, or X++ select statement), the AOS rewrites the generated Transact-SQL statement, appending an EXISTS subquery bound to the policy query.
/* Conceptual SQL generated by AOS with an active XDS Policy */
SELECT T1.ACCOUNTNUM, T1.NAME, T1.RECID
FROM CUSTTABLE T1
WHERE (T1.DATAAREAID = 'usmf')
AND EXISTS (
SELECT 1
FROM CUSTGROUP T2
JOIN DIRPERSONUSER T3 ON T2.RESPONSIBLEWORKER = T3.PERSONPARTY
WHERE T2.CUSTGROUP = T1.CUSTGROUP
AND T3.USER = 'alicia'
);
Because filtering occurs at the database query generation layer, data cannot leak through OData integrations, Excel add-ins, or custom classes, ensuring comprehensive security isolation.
[!IMPORTANT] The System Administrator Exception: Users assigned to the System Administrator role bypass all XDS policies unconditionally. When designing and testing XDS policies, developers must never test using an administrator account. Always test using a non-administrative user persona assigned the specific business roles targeted by the policy.
2. AOT Security Policy Components
Developers construct XDS policies declaratively in Visual Studio under the Security > Security Policies node of an AOT project. Every security policy consists of four fundamental structural components:
XDS Security Policy Architecture
┌─────────────────────────────────────────────────────────────┐
│ Security Policy │
├─────────────────────────────────────────────────────────────┤
│ 1. Primary Table: CustGroup │
│ 2. Policy Query: CustGroupByWorkerQuery │
│ 3. Context Type: RoleName / RoleProperty / ContextString │
│ 4. Constrained Table: Yes (Primary table constrained) │
├──────────────────────────────┬──────────────────────────────┤
│ │ Relates to
│ ▼
│ ┌───────────────────────────────────────────────────────┐ │
│ │ Constrained Tables Sub-Node │ │
│ │ • CustTable (CustTable.CustGroup == CustGroup.Group) │ │
│ │ • SalesTable (SalesTable.CustGroup == CustGroup.Group│ │
│ └───────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
Core Properties of a Security Policy
- Primary Table (
PrimaryTable): The anchor table of the policy. It serves as the root data source of the associated policy query. All filtering criteria ultimately evaluate against this table. - Policy Query (
Query): A standard AOT Query that retrieves the set of records in the Primary Table that the current user is permitted to see. The query typically incorporates ranges based on session variables (e.g.,(currentWorker())or(curUserId())). - Constrained Table (
ConstrainedTable): A boolean property on the policy itself. If set toYes, the Primary Table is automatically filtered whenever queried by affected users. If set toNo, the primary table remains unfiltered, and filtering only applies to tables listed in the Constrained Tables child node. - Constrained Tables Node: A collection of additional tables that should be filtered by the policy. For each constrained table, developers define the table relation or join condition linking the constrained table to the Primary Table.
- Operation (
Operation): Defines which database actions trigger the policy:AllOperations: ConstrainsSELECT,UPDATE,DELETE, andINSERTvalidation.Select: Constrains read queries only.Insert: Constrains record insertion.Update: Constrains record modifications.Delete: Constrains record deletions.
Comparison of Policy Operations
| Operation Value | SELECT Constrained? | UPDATE Constrained? | DELETE Constrained? | INSERT Constrained? |
|---|---|---|---|---|
AllOperations | Yes | Yes | Yes | Yes (Validation) |
Select | Yes | No | No | No |
Update | No | Yes | No | No |
Delete | No | No | Yes | No |
Insert | No | No | No | Yes |
3. Context Types & Programmatic Context Switching
The ContextType property controls when and for whom the security policy is active. Dynamics 365 Finance and Operations supports three policy context types:
| Context Type | Activation Mechanism | Use Case |
|---|---|---|
RoleName | The policy is tied to a single specific security role designated in the RoleName property. | Dedicated operational roles (e.g., Retail Store Associate can only see their retail channel). |
RoleProperty | The policy applies to any security role whose ContextString property matches the policy's ContextString. | Multi-role policies (e.g., applying regional filtering across Sales Clerk, Sales Manager, and Customer Rep). |
ContextString | The policy remains dormant until explicitly activated programmatically in X++ code using XDS::SetContext(). | Controlled process execution (e.g., applying row-level constraints only during a specific wizard or batch run). |
Programmatic Context Switching in X++
When an XDS policy has ContextType = ContextString, the AOS does not apply the policy during normal navigation. Instead, developers activate the policy in code by calling the static method XDS::SetContext():
/// <summary>
/// Demonstrates programmatic XDS context activation and cleanup.
/// </summary>
class ConRegionalDataProcessor
{
public static void processRegionalInvoices(str _regionContextCode)
{
// Activate the XDS security policy bound to this ContextString
XDS::SetContext(_regionContextCode);
try
{
CustInvoiceJour custInvoiceJour;
// This select statement is now dynamically constrained by the XDS policy
while select custInvoiceJour
where custInvoiceJour.InvoiceDate == today()
{
info(strFmt("Processing invoice %1 for customer %2",
custInvoiceJour.InvoiceId,
custInvoiceJour.InvoiceAccount));
}
}
finally
{
// Critical: Clear the XDS context to avoid leaking constraints across the session
XDS::SetContext('');
}
}
}
[!CAUTION] Session Leakage Hazard:
XDS::SetContext()modifies the security context for the current user's entire session thread. If code fails to clear the context (XDS::SetContext('')) in afinallyblock, all subsequent forms and operations opened by the user will continue to execute under the restricted XDS context, leading to missing data and unexplainable application errors.
4. Performance Optimization, Index Alignment & Caching
Because XDS rewrites SQL statements executed against constrained tables, a poorly designed security policy can severely degrade database performance across the entire environment.
Index Alignment
When the AOS injects an EXISTS JOIN linking a constrained table (e.g., CustTrans) to a primary table (e.g., CustTable), the SQL Server optimizer must join those two tables on every single query. If the join fields are not covered by an index, SQL Server will execute a full table scan on multi-million row tables.
Index Alignment for XDS Join Performance
Constrained Table: CustTrans Primary Table: CustTable
┌──────────────────────────────┐ ┌──────────────────────────────┐
│ AccountNum (Foreign Key) │──────────>│ AccountNum (Primary Key) │
│ TransDate │ JOIN │ CustGroup │
│ Voucher │ │ Currency │
└──────────────────────────────┘ └──────────────────────────────┘
▲ ▲
│ Mandatory Database Index │ Mandatory Database Index
└──────────────────────────────────────────┴─────────────────────────────
Must have composite index on: Must have index on:
[DataAreaId, AccountNum] [DataAreaId, AccountNum]
Avoiding Outer Joins in Policy Queries
Policy queries must strictly utilize Inner Joins or Exists Joins. Developers must never use Outer Joins inside an XDS policy query. Outer joins force the SQL Server query engine to preserve non-matching rows and evaluate NULL logic across the join, which prevents the optimizer from using index seeks and frequently results in query plan hash joins, deadlocks, and query timeouts.
Table Caching Interactions
Dynamics 365 Finance and Operations implements multiple levels of Application Object Server caching:
EntireTableCache: The AOS loads the entire table into server memory on the first read. Never apply an XDS policy to a table withCacheLookup = EntireTable! If an XDS policy is applied, the global in-memory cache conflicts with user-specific row filtering. The system must repeatedly invalidate and reload the cache, creating massive CPU spikes and thread locking.FoundandFoundAndEmptyCache: Individual record caches are security-aware. The cache key includes the security context, allowing XDS to function safely with single-record lookups.
5. Scenario Walk-Through: Multi-Regional Sales Security Policy
Business Context
Contoso Retail operates stores across three sales territories: East, Central, and West. The corporate compliance board establishes the following security rule:
"Sales clerks must only view and edit customer records and sales orders belonging to their assigned territory's Customer Group. Under no circumstances may a sales clerk see customers or sales orders from other territories."
Implementation Walk-Through
Contoso Territory Policy Implementation Structure
┌─────────────────────────────────────────────────────────────────────────────┐
│ 1. AOT Query: ConTerritoryCustGroupQuery │
│ Data Source: CustGroup │
│ Child Data Source: ConTerritoryWorkerAssignment (Inner Join) │
│ Range on Worker: (currentWorker()) │
└──────────────────────────────────────┬──────────────────────────────────────┘
│ Used as Policy Query
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ 2. AOT Security Policy: ConTerritoryCustomerPolicy │
│ Primary Table: CustGroup │
│ Query: ConTerritoryCustGroupQuery │
│ Context Type: RoleName │
│ Role Name: ConSalesClerkRole │
│ Constrained Table: Yes (CustGroup itself is constrained) │
├─────────────────────────────────────────────────────────────────────────────┤
│ 3. Constrained Tables Sub-Node │
│ • Table: CustTable │
│ Relation: CustTable.CustGroup == CustGroup.CustGroup │
│ • Table: SalesTable │
│ Relation: SalesTable.CustGroup == CustGroup.CustGroup │
└─────────────────────────────────────────────────────────────────────────────┘
- Create the Policy Query (
ConTerritoryCustGroupQuery):- Root data source:
CustGroup. - Joined data source:
ConTerritoryWorkerAssignment, joined onCustGroup.TerritoryId == ConTerritoryWorkerAssignment.TerritoryId. - Query range on
ConTerritoryWorkerAssignment.Worker: Value =(currentWorker()).
- Root data source:
- Create the Security Policy (
ConTerritoryCustomerPolicy):- Set
PrimaryTable = CustGroup. - Set
Query = ConTerritoryCustGroupQuery. - Set
ContextType = RoleNameand setRoleName = ConSalesClerkRole. - Set
ConstrainedTable = Yes.
- Set
- Add Constrained Tables:
- Add
CustTable. Set relation toCustTable.CustGroup == CustGroup.CustGroup. - Add
SalesTable. Set relation toSalesTable.CustGroup == CustGroup.CustGroup.
- Add
- Verification & Index Alignment:
- Confirm that
CustTablehas an index covering[DataAreaId, CustGroup]. - Confirm that
SalesTablehas an index covering[DataAreaId, CustGroup]. - Log in as a test user assigned to
ConSalesClerkRole(not System Administrator) and verify that only territory-specific customers and sales orders render.
- Confirm that
6. Advanced Pattern: The "MyTable" Pattern and Cross-Company XDS
In complex multi-company environments, filtering criteria frequently span multiple legal entities or depend on dynamic organizational hierarchy positions that are expensive to evaluate repeatedly inside SQL queries.
The "MyTable" Design Pattern
To avoid joining massive HR and hierarchy tables on every single constrained query, developers implement the MyTable pattern (e.g., MyLegalEntities, MyCostCenters, MyDepartments):
- A temporary table or lightweight persisted session table is created containing the list of entity IDs accessible to the current user.
- When the user logs in or begins a session, a startup routine populates the user's
MyTablerecords. - The XDS Policy Query simply joins the Primary Table directly to
MyTablewhereMyTable.UserId == curUserId(). - Because
MyTablecontains only the current user's authorized IDs, the resulting SQL query plan is exceptionally lightweight, converting complex hierarchy evaluations into a simple indexed primary key lookup.
7. Exam Traps & Troubleshooting Reference
| Issue / Trap | Root Cause | Resolution |
|---|---|---|
| Admin sees all data | Developer tests XDS while assigned the System Administrator role. | System Administrators bypass XDS by design. Test exclusively with a non-admin test user account. |
| Severe query slowdowns | Join fields between constrained table and primary table lack database indexes. | Add composite indexes on the foreign key join fields in the constrained table. |
| AOS CPU spikes on lookup | XDS policy is applied to a table configured with CacheLookup = EntireTable. | Remove the XDS constraint or change table caching strategy to Found or None. |
| Context leaking across forms | Developer called XDS::SetContext() without clearing it in a finally block. | Always invoke XDS::SetContext('') inside a structured finally block. |
| Blank lookup dropdowns | Primary table has ConstrainedTable = Yes and the user has no matching query rows. | Verify that the policy query returns valid master records for the active worker identity. |
| Outer join query timeout | The policy query uses an Outer Join between data sources. | Refactor the query to use InnerJoin or ExistsJoin exclusively. |
A developer creates a new Extensible Data Security (XDS) policy in Visual Studio to restrict sales representatives so they can only see customers in their assigned sales territory. The developer tests the policy in the development environment, but notices that all customer records across all territories are still visible on the All Customers form. What is the most likely reason for this behavior?
An architect is designing an XDS policy that must restrict access to sensitive general ledger journals. However, the policy should NOT apply during general system navigation; it must only apply during the execution of a specific month-end financial closing batch routine authored in X++. Which Context Type should the developer specify on the security policy?
After deploying an XDS policy that constrains the high-volume CustTrans table based on customer account groups, users report that opening customer transaction inquiries causes severe database timeouts and high DTU utilization on Azure SQL Database. Inspection reveals that SQL Server is performing full table scans on CustTrans. What is the primary architectural remediation required to resolve this performance defect?
A developer needs to configure an AOT Security Policy on the VendTable (Vendors) table. The policy query filters vendor records based on the user's purchasing department. The developer wants both the VendTable itself and the child PurchTable (Purchase Orders) to be filtered automatically by this single policy. How should the developer configure the policy properties in the AOT?