Table data access requires explicit object permissions

Properties
AC0031 Info Design Code Fix Ignore Obsolete

When a codeunit, report, query, or xmlport accesses table data without declaring the required permissions, the runtime relies on the calling user’s direct permissions. This breaks indirect permission scenarios, where a user’s license grants only indirect access to a table (for example, indirect write on ledger entry tables or indirect read on the Sent Emails table). Declaring Permissions = tabledata ... = rimd on the object allows the runtime to grant access through the object, regardless of the user’s direct license assignment.

Example

codeunit 50100 "Post Sales Invoice"
{
    procedure Post()
    var
        SalesHeader: Record "Sales Header";
    begin
        SalesHeader.FindFirst(); // Table data access requires explicit object permissions [AC0031]
    end;
}

Add the Permissions property to the containing object:

codeunit 50100 "Post Sales Invoice"
{
    Permissions = tabledata "Sales Header" = r;

    procedure Post()
    var
        SalesHeader: Record "Sales Header";
    begin
        SalesHeader.FindFirst();
    end;
}

When the diagnostic is reported

The following record method calls trigger this diagnostic:

PermissionMethods
Read (r)Find, FindFirst, FindLast, FindSet, Get, GetBySystemId, IsEmpty, Count, Next
Insert (i)Insert
Modify (m)Modify, ModifyAll, Rename
Delete (d)Delete, DeleteAll

Calls through the implicit Rec variable inside table objects are included (for example, Rec.Modify() in a table procedure).

Next counts as a read even when the record set was positioned by another object. Permissions do not flow through the call stack: when a helper procedure runs FindSet on a var record parameter and the caller iterates it with repeat ... until Rec.Next() = 0, each Next call reads the table on behalf of the calling object, so the caller needs its own read (r) permission.

codeunit 50100 "Customer Processing"
{
    Permissions = tabledata Customer = r; // required: Next() reads Customer from this object

    procedure ProcessAll()
    var
        Customer: Record Customer;
        CustomerQuery: Codeunit "Customer Query";
    begin
        if CustomerQuery.FindActive(Customer) then
            repeat
                // ...
            until Customer.Next() = 0;
    end;
}

Report and query dataitem elements require read (r) permissions for their source table.

XmlPort tableelement nodes require permissions based on the Direction property:

DirectionRequired permissions
ImportInsert (i) when AutoSave = true (default), Modify (m) when AutoReplace = true or AutoUpdate = true (defaults)
ExportRead (r)
BothAll of the above

Setting AutoSave, AutoReplace, or AutoUpdate to false suppresses the corresponding requirement.

DataTransfer operations

A DataTransfer variable performs its database work in a single bulk statement when CopyFields or CopyRows is called. The tables involved are the ones named in SetTables:

MethodSource tableDestination table
CopyFieldsRead (r)Modify (m)
CopyRowsRead (r)Insert (i)

The other DataTransfer methods (SetTables, AddFieldValue, AddConstantValue, AddJoin, AddSourceFilter, AddDestinationFilter, UpdateAuditFields) only configure the transfer in memory and do not require permissions. The diagnostic is reported on the CopyFields or CopyRows call.

codeunit 50100 "Upgrade Purch. Cr. Memo Line"
{
    Subtype = Upgrade;
    Permissions = tabledata "Purch. Cr. Memo Line" = r; // missing m: CopyFields modifies the destination

    procedure UpgradeCustomCode(OldValue: Code[20]; NewValue: Code[20])
    var
        PurchCrMemoLine: Record "Purch. Cr. Memo Line";
        PurchCrMemoLineDataTransfer: DataTransfer;
    begin
        PurchCrMemoLineDataTransfer.SetTables(Database::"Purch. Cr. Memo Line", Database::"Purch. Cr. Memo Line");
        PurchCrMemoLineDataTransfer.AddSourceFilter(PurchCrMemoLine.FieldNo("Custom Code"), '=%1', OldValue);
        PurchCrMemoLineDataTransfer.AddConstantValue(NewValue, PurchCrMemoLine.FieldNo("Custom Code"));
        PurchCrMemoLineDataTransfer.CopyFields(); // Table data access requires explicit object permissions [AC0031]
    end;
}

The tables come from the most recent SetTables call on the same DataTransfer variable that reaches the CopyFields/CopyRows call in the same procedure or trigger, and only when both arguments are Database::"Table Name" literals. A later SetTables replaces the earlier one, so a variable that is reconfigured between copies charges each call only for the tables it actually transfers. When branches (if, case) set different tables before the call, all of those tables count, and so does a SetTables that only reaches the call through a loop’s next iteration. The variable itself may be a local, a global (also when addressed as this.MyDataTransfer), or a parameter.

When the tables cannot be determined — no SetTables reaching the call in the same body (for example, configured in another procedure or by the caller), or an argument that is a variable, parameter, or expression instead of a Database:: literal — no diagnostic is reported for that call.

Exceptions

The diagnostic is suppressed when any of the following conditions apply:

  • The target table is a system table (ID > 2,000,000,000)
  • The codeunit is a test codeunit with TestPermissions = Disabled
  • The containing object is a permissionset or permissionsetextension (these declare permissions structurally, not for access control)

The diagnostic also recognizes multiple permission sources. If any source covers the required operation, no diagnostic is reported.

Object-level Permissions property is the primary source:

codeunit 50100 "My Codeunit"
{
    Permissions = tabledata Customer = r;

    procedure DoSomething()
    var
        Cust: Record Customer;
    begin
        Cust.FindFirst(); // No diagnostic
    end;
}

Table-level InherentPermissions property grants permissions to all callers:

table 50100 "My Table"
{
    InherentPermissions = rimd;
}

Method-level [InherentPermissions] attribute grants permissions for a single procedure:

[InherentPermissions(PermissionObjectType::TableData, Database::Customer, 'r')]
procedure ReadCustomer()

Page SourceTable exemption covers all CRUD on the page’s own source table, including in page extensions. A page that sets SourceTable = Customer does not need to declare permissions for Customer.

Namespace-qualified references (tabledata MyNamespace."My Table" = r) and object ID references (tabledata 50100 = r) are both recognized.

Temporary tables

Temporary tables never touch the database, so they never require permissions. The diagnostic is suppressed for every way a temporary table can be implemented:

  • A record variable marked with the temporary keyword (var Rec: Record Customer temporary;)
  • A table object declared with TableType = Temporary (a plain var Rec: Record MyTempTable; where MyTempTable is a temporary table)
  • A page with SourceTableTemporary = true (covered by the page SourceTable exemption)
  • A report data item or XMLPort table element with UseTemporary = true

Temporary tables don’t require the user to have permissions on the underlying table. Because temporary table data is held only in memory and never read from or written to the database, the permission system doesn’t apply. […] This behavior applies regardless of how the temporary table is implemented—whether by using the TableType property, a temporary record variable, or the SourceTableTemporary property on a page.

Temporary tables on Microsoft Learn

See also