All Around Microsoft Dynamics Business Central
The last BusinessCraft post ended with permissions as the next big construction site. They work now: system, tenant, and AL permission sets, plus inherent permissions directly on objects. On the way, BusinessCraft also got its first virtual tables: Date, Integer, Session, AllObj, and AllObjWithCaption.
In the last post, only the NAV way worked: Permission Set, Permission, and Access Control. That is one source of permissions. Business Central has three:
| Source | Where it comes from |
|---|---|
| System | Permission sets that ship with the platform, like SUPER |
| Tenant | Permission sets that users create or copy in the client |
| AL | permissionset objects in installed apps |
BusinessCraft now reads all three and aggregates them into one effective permission set per user, together with the inherent permissions described below. That is what the checks for read, insert, modify, delete, and execute run against.
A few details follow BC closely:
Metadata Permission Set and Metadata Permission, with the same table IDs as in BC (2000000250 and 2000000251). Included sets and permission set extensions are flattened into them.Aggregate Permission Set is not stored. It is projected from the three sources whenever it is read, so it can never be out of date.An AL permission set is an object like any other:
permissionset 50100 "SALES BASIC"
{
Assignable = true;
Caption = 'Sales Basic';
Permissions =
tabledata "Sales Header" = RIMD,
tabledata "Sales Line" = RIMD,
page "Sales Order" = X;
}
Permissions do not have to come from a permission set. With InherentPermissions, an object can grant permissions on itself:
table 50100 "Sales Setup"
{
InherentPermissions = R;
...
}
Every user can read this table now, even if no permission set mentions it. This is the modern way in BC for things like setup tables, which every user needs to read anyway.
BusinessCraft parses InherentPermissions on tables, pages, and codeunits and adds them to the effective permissions, in addition to the permission sets the user has.
Indirect permissions are parsed, but not tested yet.
The idea in BC: a user with indirect permission on a table may not touch it directly, only through an object that grants access with its Permissions property. A typical example is a posting codeunit that writes ledger entries, while the user can only read them. That needs a proper test before I call it done.
The last post said it already: the permission pages in BC use a different corner of the runtime on every page. That was true. Most of the work was not the permission logic itself, but getting the triggers right and passing state through them cleanly.
Some examples:
Page.Run with a temporary record. The permission set list works on a temporary buffer. The temporary records now travel into pages with SourceTableTemporary, and OnInit runs before the page opens. That is what hides the tenant permission subform for system sets.OnInsertRecord returning false. On a temporary source table, the page inserts the record itself and returns false. The shared Rec buffer has to survive that.CurrPage.Editable. The tenant permission subform decides in ActivateControls whether it can be edited, so it needs to read this.User.Get in OnNewRecord and User.TestField in OnInsertRecord. Without the record from the first trigger, the second one failed. The same goes for flags like IsPermissionSetEditable, which were wiped on OnValidate and blocked editing the name.Rec until the trigger finishes. Field writes in OnValidate and OnInsert were lost when the cursor was released too early. This one did not only affect permissions: it also dropped the Item No. validation on sales lines and number series assignments.LookupPageId and TableRelation on the page field, so choosing Yes or Indirect from the lookup list writes the value and runs OnValidate.CurrPage.Caption changes the page caption at runtime, and the delete confirmation for a permission line now shows object type and ID instead of the permission set.0, and the first Object Type option has the value 0 too. Both have to count as real values, not as blank.None of this is permission logic. But without it, the BC permission pages do not run.
Virtual tables are tables that have no rows in the database. The platform computes them on the fly. Every BC developer uses them, often without thinking about it:
Integer for loops in reports, like copies or blank linesDate for periods: days, weeks, months, quarters, yearsAllObj and AllObjWithCaption for object lookupsSession for the sessions on the serverBusinessCraft now implements these five.
These two were the easy ones. BusinessCraft already knows every object in every installed app, so AllObj simply lists them with object type, ID, name, and app. AllObjWithCaption adds the caption, translated into the session language with the XLIFF files from the earlier posts.
These two were harder than expected.
The first version did the obvious thing: generate all values when the table is opened. For Integer, that is every number from roughly minus two billion to plus two billion. For Date, every day, week, month, quarter, and year across the whole calendar. It worked in theory, but it ate the performance.
The fix is a window. BusinessCraft only generates the rows that are actually needed. FindFirst, FindLast, Next, and filters are calculated directly, without a list behind them. When the client asks for the next 50 rows, it gets exactly 50 rows. This works very well now, also with filters like these:
Integer.SetRange(Number, 1, NoOfCopies);
Date.SetRange("Period Type", Date."Period Type"::Month);
Date.SetRange("Period Start", DMY2Date(1, 1, 2026), DMY2Date(31, 12, 2026));
The web client needed an adjustment too.
Until now, a list started at the first record and only loaded more rows when you scrolled down. That is fine for customers or items. It is not fine for Integer.
In Business Central, a list on Integer opens at 0. But 0 is not the first record. You can scroll up into the negative numbers. So the client now also loads rows before the first loaded row, when you scroll up. Same behavior as in BC.
The Session table lists the active sessions on the server.
And it does a bit more: deleting a record ends that session. The user of that session is logged out. Your own session cannot be ended that way. That would be a strange way to log out.
A small thing, but you notice it on every card: the page caption.
BusinessCraft now uses DataCaptionFields on the table and DataCaptionExpression on the page to build the caption, like BC does. The card shows which record you are looking at, not just the page name.
The repeater in a ListPart, like the lines on a sales order, got an overhaul.
Until now, there was a New button to add a line. It is gone. The logic is now the same as in Business Central:
This sounds like a detail, but it is how everyone enters sales lines in BC. With a New button in between, it never felt right.
List pages now have the view/edit toggle action, like in BC. An editable list opens in view mode, and you switch to edit mode when you want to change values directly in the list.
Date, Integer, Session, AllObj, and AllObjWithCaptionPermissions work, but one part is still open: indirect permissions. They are parsed, and now they need a real test. A user who can only read the item ledger entries should still be able to post a sales order, because Sales Post writes the entries on their behalf. And the same user must not be able to insert an entry directly.
If that works, the permission system is complete. I'll keep you updated. :)
BusinessCraft is not affiliated with or endorsed by Microsoft. "Microsoft", "Dynamics 365" and "Business Central" are trademarks of Microsoft Corporation.