Table data access requires explicit object permissions
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:
| Permission | Methods |
|---|---|
Read (r) | Find, FindFirst, FindLast, FindSet, Get, GetBySystemId, IsEmpty, Count, Next |
Insert (i) | Insert |
Modify (m) | Modify, ModifyAll, Rename |
Delete (d) | Delete, DeleteAll |
ModifyAll and DeleteAll also need read (r), because they select the affected rows first: ModifyAll requires rm and DeleteAll requires rd. Modify, Delete and Insert need only their own permission. A Permissions = tabledata "Sales Line" = d entry next to a DeleteAll therefore reports a missing r.
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:
| Direction | Required permissions |
|---|---|
| Import | Insert (i) when AutoSave = true (default), Modify (m) when AutoReplace = true or AutoUpdate = true (defaults) |
| Export | Read (r) |
| Both | All 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:
| Method | Source table | Destination table |
|---|---|---|
CopyFields | Read (r) | Modify (m) |
CopyRows | Read (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
permissionsetorpermissionsetextension(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.
Object-level AccessByPermission property on a report or page covers the permissions in its mask for the table it names. The property hides the object from users who lack those permissions, so an object that declares it is meant for users who already hold them and does not rely on indirect access through the object:
report 50100 "Process Incoming Documents"
{
ApplicationArea = All;
UsageCategory = Tasks;
ProcessingOnly = true;
AccessByPermission = tabledata "Incoming Document" = RM;
dataset
{
dataitem(IncomingDocument; "Incoming Document") // No diagnostic: R is in the mask
{
}
}
}
Only the permissions in the mask count, regardless of casing: with = RD, reads and deletes on the table are covered but a Modify still reports a missing m. The table X = X form (permission to run an object) and entries for other tables cover nothing. AccessByPermission on individual fields, actions and parts is not considered.
AccessByPermission does not grant permissions at runtime. The check passes when the user has at least one of the permissions in the mask, it only applies when the server’s UI elements removal setting is LicenseFile or LicenseFileAndUserPermissions, and the object can still be run from code or from an action with RunObject. Add the Permissions property when the object must work for users who have only indirect permissions on the table.
Namespace-qualified references (tabledata MyNamespace."My Table" = r) and object ID references (tabledata 50100 = r) are both recognized.
Extension objects
The diagnostic is also reported in table and page extensions, for example when a procedure or trigger in a pageextension reads a table other than the page’s SourceTable. These accesses cannot be covered in place:
- Extension objects cannot declare the
Permissionsproperty. - The method-level
[InherentPermissions]attribute only accepts objects from the same extension, so pointing it at a Base Application table fails with AL0720 .
For the same reason, the code fix is not offered on extension objects. To comply, move the data access into an object that can declare Permissions, such as a codeunit, and call it from the extension.
Allowing the Permissions property on extension objects is an open request to Microsoft: Set permissions on page- and table extensions
. It is marked as needing votes and is not on Microsoft’s roadmap yet, so add your vote if this affects you.
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
temporarykeyword (var Rec: Record Customer temporary;) - A table object declared with
TableType = Temporary(a plainvar Rec: Record MyTempTable;whereMyTempTableis a temporary table) - A page with
SourceTableTemporary = true(covered by the pageSourceTableexemption) - 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
TableTypeproperty, a temporary record variable, or theSourceTableTemporaryproperty on a page.
— Temporary tables on Microsoft Learn
See also
- Permissions on database objects on Microsoft Learn
- InherentPermissions property on Microsoft Learn
- Indirect permissions on Microsoft Learn
- Set permissions on page- and table extensions on Dynamics 365 Ideas