Table data access requires permissions (AC0031)

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.

A code fix is available for this diagnostic.

Example

codeunit 50100 "Post Sales Invoice"
{
    procedure Post()
    var
        SalesHeader: Record "Sales Header";
    begin
        SalesHeader.FindFirst(); // Table data access requires permissions: 'r' for 'Sales Header' [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;
}

Detected operations

The following record method calls trigger this diagnostic:

PermissionMethods
Read (r)Find, FindFirst, FindLast, FindSet, Get, GetBySystemId, IsEmpty, Count
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).

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.

When the rule does not trigger

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

  • The record variable is marked as Temporary
  • The target table is a system table (ID > 2,000,000,000)
  • The containing symbol is obsolete
  • 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.

Pragmas and comments embedded within the Permissions property value do not affect resolution.

Extension objects

Extension objects cannot declare the Permissions property. If you receive this diagnostic on an extension object, move the data access code to a codeunit where you can declare the required permissions.

Code fix

The ALCops: Add permission code fix adds the missing tabledata entry to the Permissions property. It handles three scenarios:

No Permissions property exists. The fix creates the property:

// Before
codeunit 50100 "My Codeunit"
{
    procedure DoSomething()
    var
        Cust: Record Customer;
    begin
        Cust.FindFirst();
    end;
}

// After
codeunit 50100 "My Codeunit"
{
    Permissions = tabledata Customer = r;
    procedure DoSomething()
    var
        Cust: Record Customer;
    begin
        Cust.FindFirst();
    end;
}

Table already listed but missing a permission char. The fix merges the char in canonical rimd order:

// Before: has 'r', needs 'm'
Permissions = tabledata Customer = r;

// After
Permissions = tabledata Customer = rm;

Table not yet listed. The fix adds a new entry, preserving the existing format (single-line or multi-line). When the existing entries are alphabetically sorted, the new entry is inserted in order. Otherwise it is appended.

// Before (multi-line, sorted)
Permissions = tabledata Customer = r,
              tabledata "Sales Header" = rm;

// After adding 'Item'
Permissions = tabledata Customer = r,
              tabledata Item = r,
              tabledata "Sales Header" = rm;

The code fix supports Fix All to apply all missing permissions in one operation. It is not offered for extension objects.

See also