Unused permission declared
The Permissions property declares which table data operations the object performs. When an entry references a table the object never accesses, or grants broader access than the code requires, the extra declarations mislead anyone auditing the object: a reviewer reading Permissions = tabledata "G/L Entry" = rimd trusts that the object modifies and deletes general ledger entries, when in fact it only reads them. Remove unused entries and reduce overly broad chars to match actual usage.
This rule is the inverse of AC0031 . Where AC0031 detects missing permissions, AC0032 detects permissions that are declared but not backed by any code.
Example
Entire entry unused. When a Permissions entry references a table that no code in the object accesses at all:
codeunit 50100 "Post Sales Invoice"
{
Permissions = tabledata "Sales Header" = rimd; // Unused permission declared [AC0032]
procedure Post()
begin
// No code accesses "Sales Header"
end;
}Remove the unused entry:
codeunit 50100 "Post Sales Invoice"
{
procedure Post()
begin
end;
}Partial chars unused. When the declared permission chars exceed what the code actually needs:
codeunit 50100 "Post Sales Invoice"
{
Permissions = tabledata "Sales Header" = rimd; // Unused permission declared [AC0032]
procedure Post()
var
SalesHeader: Record "Sales Header";
begin
SalesHeader.FindFirst();
end;
}Reduce the permission to match actual usage:
codeunit 50100 "Post Sales Invoice"
{
Permissions = tabledata "Sales Header" = r;
procedure Post()
var
SalesHeader: Record "Sales Header";
begin
SalesHeader.FindFirst();
end;
}Code fix
The ALCops: Remove unused permission code fix handles both variants:
- Entire entry unused: removes the entry from the list. If it is the last entry, the entire
Permissionsproperty is removed. - Partial chars unused: reduces the permission string to only the chars that are actually used, in canonical
rimdorder.
Exception
The diagnostic is suppressed when any of the following conditions apply:
- 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
permissionsetorpermissionsetextension(these declare permissions structurally, not for access control) - The object contains a database operation on a
RecordRef(see below) - The object contains a
DataTransferCopyFieldsorCopyRowscall whose tables cannot be resolved (see below)
Record sets positioned by another object
Next counts as a read (r) operation. Permissions do not flow through the call stack, so when a helper procedure runs FindSet on a var record parameter and the calling object only iterates the result with repeat ... until Rec.Next() = 0, the caller’s read permission is in use: every Next call fetches rows from the table on behalf of the calling object. AC0032 does not report such an entry as unused.
codeunit 50100 "Customer Processing"
{
// Not reported: Customer.Next() below is a read on Customer performed by this object
Permissions = tabledata Customer = r;
procedure ProcessAll()
var
Customer: Record Customer;
CustomerQuery: Codeunit "Customer Query";
begin
if CustomerQuery.FindActive(Customer) then
repeat
// ...
until Customer.Next() = 0;
end;
}RecordRef operations
A RecordRef can point to any table, and which table it targets is only known at runtime. When an object contains any database operation on a RecordRef receiver (Find, FindFirst, FindLast, FindSet, Get, GetBySystemId, IsEmpty, Count, Next, Insert, Modify, ModifyAll, Rename, Delete, or DeleteAll), the rule cannot determine which declared permission that operation consumes. AC0032 is therefore disabled for the entire object: no unused-permission diagnostics are reported, and the code fix is not offered.
codeunit 50100 "Ledger Entry Management"
{
// No AC0032 diagnostics: the RecordRef.Modify call below may target any of these tables at runtime
Permissions =
tabledata "G/L Entry" = md,
tabledata "Cust. Ledger Entry" = md;
procedure DoModify(var RecordRefToModify: RecordRef; RunTrigger: Boolean)
begin
RecordRefToModify.Modify(RunTrigger);
end;
}Note that this also suppresses genuinely unused entries in the same object. This trade-off is deliberate: a false “unused” report combined with the code fix would remove permissions that are required at runtime.
DataTransfer operations
A DataTransfer variable touches the database only when CopyFields or CopyRows is called, and the tables involved are the ones named in SetTables. These calls count as using the following permissions:
| 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) configure the transfer in memory and do not count as usage. Chars beyond what the executor needs are still reported: tabledata X = rimd with only a same-table CopyFields reports id as unused.
codeunit 50100 "Upgrade Purch. Cr. Memo Line"
{
Subtype = Upgrade;
// Not reported: CopyFields reads and modifies "Purch. Cr. Memo Line"
Permissions = tabledata "Purch. Cr. Memo Line" = rm;
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();
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 only the tables a given call actually transfers count as used by it — permissions granted for a table that some other copy in the same procedure transfers are still reported as unused. 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 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 — the operation may target any table, and AC0032 is disabled for the entire object, exactly as for RecordRef operations above.
Temporary tables
Access through a temporary table does not count as using a permission. Temporary tables never touch the database, so a Permissions entry for a table that is accessed only through temporary records is dead code and is reported as unused. This applies to every way a temporary table can be implemented: the temporary keyword on a record variable, a table object with TableType = Temporary, a page with SourceTableTemporary = true, and report data items or XMLPort table elements 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