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.
Last updated: September 2026

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 JOIN or subquery to every SQL statement executed against constrained tables. Policies are activated based on their Context Type: RoleName, RoleProperty, or programmatically via ContextString using XDS::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

  1. 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.
  2. 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())).
  3. Constrained Table (ConstrainedTable): A boolean property on the policy itself. If set to Yes, the Primary Table is automatically filtered whenever queried by affected users. If set to No, the primary table remains unfiltered, and filtering only applies to tables listed in the Constrained Tables child node.
  4. 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.
  5. Operation (Operation): Defines which database actions trigger the policy:
    • AllOperations: Constrains SELECT, UPDATE, DELETE, and INSERT validation.
    • Select: Constrains read queries only.
    • Insert: Constrains record insertion.
    • Update: Constrains record modifications.
    • Delete: Constrains record deletions.

Comparison of Policy Operations

Operation ValueSELECT Constrained?UPDATE Constrained?DELETE Constrained?INSERT Constrained?
AllOperationsYesYesYesYes (Validation)
SelectYesNoNoNo
UpdateNoYesNoNo
DeleteNoNoYesNo
InsertNoNoNoYes

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 TypeActivation MechanismUse Case
RoleNameThe 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).
RolePropertyThe 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).
ContextStringThe 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 a finally block, 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:

  • EntireTable Cache: The AOS loads the entire table into server memory on the first read. Never apply an XDS policy to a table with CacheLookup = 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.
  • Found and FoundAndEmpty Cache: 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                   │
└─────────────────────────────────────────────────────────────────────────────┘
  1. Create the Policy Query (ConTerritoryCustGroupQuery):
    • Root data source: CustGroup.
    • Joined data source: ConTerritoryWorkerAssignment, joined on CustGroup.TerritoryId == ConTerritoryWorkerAssignment.TerritoryId.
    • Query range on ConTerritoryWorkerAssignment.Worker: Value = (currentWorker()).
  2. Create the Security Policy (ConTerritoryCustomerPolicy):
    • Set PrimaryTable = CustGroup.
    • Set Query = ConTerritoryCustGroupQuery.
    • Set ContextType = RoleName and set RoleName = ConSalesClerkRole.
    • Set ConstrainedTable = Yes.
  3. Add Constrained Tables:
    • Add CustTable. Set relation to CustTable.CustGroup == CustGroup.CustGroup.
    • Add SalesTable. Set relation to SalesTable.CustGroup == CustGroup.CustGroup.
  4. Verification & Index Alignment:
    • Confirm that CustTable has an index covering [DataAreaId, CustGroup].
    • Confirm that SalesTable has 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.

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):

  1. A temporary table or lightweight persisted session table is created containing the list of entity IDs accessible to the current user.
  2. When the user logs in or begins a session, a startup routine populates the user's MyTable records.
  3. The XDS Policy Query simply joins the Primary Table directly to MyTable where MyTable.UserId == curUserId().
  4. Because MyTable contains 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 / TrapRoot CauseResolution
Admin sees all dataDeveloper 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 slowdownsJoin 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 lookupXDS 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 formsDeveloper called XDS::SetContext() without clearing it in a finally block.Always invoke XDS::SetContext('') inside a structured finally block.
Blank lookup dropdownsPrimary 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 timeoutThe policy query uses an Outer Join between data sources.Refactor the query to use InnerJoin or ExistsJoin exclusively.
Loading diagram...
AOS Extensible Data Security Query Rewriting Pipeline
Test Your Knowledge

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?

A
B
C
D
Test Your Knowledge

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?

A
B
C
D
Test Your Knowledge

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
B
C
D
Test Your Knowledge

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?

A
B
C
D