There are four layers between a user and a table: the licence, the entitlement, the permission sets and the object's own access level. Most permission problems come from not knowing which layer said no. How each works, how to ship permission sets with an extension, why SUPER for everyone is not a plan, and how agents changed the picture.
"The user cannot see the page" is the most common ticket on any Business Central support desk after go-live, and it is nearly always a permissions question with four possible answers. This is the model, layer by layer, and how an extension developer and a system administrator should each use it.
Each user has a licence (Essentials, Premium, Team Member, Device, External Accountant, and the system ones). Microsoft ships an entitlement object per licence that lists the maximum the licence may do: a Team Member's entitlement does not include inserting sales documents, however many permission sets you give them. This layer is Microsoft's and you cannot change it. When a Team Member "has the permission set but still gets an error", this is why. Who needs which licence covers the choice.
A permission set is a named list of permissions: for each object (table data, table, page, codeunit, report, query, XMLport, system), which of Read, Insert, Modify, Delete and Execute are allowed, directly or indirectly (indirect means "only when called from code that has direct permission", which is how a user can post a sales order and write to G/L Entry without being able to edit G/L entries). Permission sets can include other permission sets, and exclude specific permissions, so you compose a role from building blocks.
Users are assigned permission sets per company (or all companies), and user groups (now "security groups", linked to Entra groups) assign sets to many users at once. The effective permission is the union of everything assigned. SUPER is the set that grants everything; it is for administrators and for nobody else, and "give them SUPER until we sort the permissions out" is the start of every permissions problem we are asked to untangle.
An extension that adds tables and pages has to say who may use them, or nobody can except SUPER. Since version 18 permission sets are AL objects:
permissionset 50100 "CON COURIER"
{
Assignable = true;
Caption = 'Contoso Courier';
Permissions =
tabledata "CON Courier Shipment" = RIMD,
table "CON Courier Shipment" = X,
page "CON Courier Shipments" = X,
page "CON Courier Shipment Card" = X,
codeunit "CON Courier Shipment Mgt." = X,
report "CON Despatch Label" = X;
}
permissionsetextension 50101 "CON Sales Extends D365 SALES" extends "D365 SALES"
{
Permissions = tabledata "CON Courier Shipment" = R;
}
Ship one assignable set per feature area, plus one that includes them all, and use permission set extensions to add your objects to Microsoft's standard sets (D365 BASIC, D365 SALES) so that existing roles keep working the day your app is installed. Mark internal-only sets Assignable = false. The AL permission-set generation tool (from the extension's objects) gets you a starting point; it grants too much, so trim it. And test as a user who is not SUPER; the pipeline should run the tests under a restricted permission set, which the test runner supports.
Independent of permissions, an object can declare who may reference it: Access = Public (default) or Internal (only this app, or apps listed in internalsVisibleTo). Internal is a compile-time boundary for developers, not a runtime security boundary; it is for keeping your implementation details out of other apps' reach, not for protecting data. Extensible = false on a table or page similarly stops other apps extending it. Use them to keep your API surface small.
The error text tells you. "You do not have the following permissions on TableData X: Insert" is layer 2 (or 3): a missing permission set. "Your license does not allow..." or a Team Member who has the set and still cannot post is layer 1. An AL compile error "cannot access internal object" is layer 4. The Effective Permissions page (from the user card) shows what a user actually has on an object and which set granted it, and is the first place an administrator should look, before changing anything.
Security filters on a permission set restrict a user to records matching a filter (this salesperson sees only their customers). They are powerful and they are easy to make slow, because the filter applies to every read, so keep them on indexed fields. There is no native field-level security; hiding a field on a page is not security, and the API will still return it. If a field must be invisible to some users, that is a design question for a separate table.
An AI agent (Sales Order Agent, Payables Agent) runs as its own user with its own permission sets, and it only does what those sets allow. That is the control: a Payables Agent that can create purchase invoices but not post them is enforced by permissions, not by prompt. It also means an extension that assumed every session was a human with a broad set may fail for an agent; ship permission sets that cover what the agent needs and test with an agent-shaped user.
Permissions are tedious and they are where "the system is secure" is actually true or not. If an extension you have inherited ships no permission sets, or every user has SUPER, both are findings in our health check, and both are a day to fix.