Exclude Permissions, you should know this by now!

With Business Central 2022 Wave 2, we got Exclude Permissions, but many people, including myself, tried it and failed due to a few bumps on the way. In this video, I show you how easy it works. Check it out:

https://youtu.be/XROXruTp2fQ

In this video, Erik walks through one of the most underutilized features in Business Central: Exclude Permissions. Introduced in version 21, this feature allows administrators to take a broad permission set (like Microsoft’s D365 Business Full Access) and surgically remove specific permissions — without ever modifying the original set. Erik demonstrates the concept live with a dual-screen setup, showing how to create a custom permission set that includes everything from Microsoft’s full access set but excludes specific pages and table data operations.

A Rocky Start: The History of Exclude Permissions

Exclude permissions arrived with version 21 of Business Central, but the rollout wasn’t smooth. Erik recounts the timeline:

  • Version 21.0: A bug prevented exclude permissions from working at all.
  • Version 21.1: The initial bug was fixed, but a critical limitation remained — you couldn’t exclude from wildcard permissions (e.g., permission sets that included Page 0, meaning “all pages”).
  • Version 21.3/21.4: The wildcard exclusion issue was resolved, and exclude permissions finally worked as expected.

Now, well into version 22 and beyond, Erik notes that many people — including developers — still aren’t familiar with how exclude permissions work. That’s the motivation for this walkthrough.

The Core Concept: Exception-Based Security

In the old days, if you wanted a user to have access to almost everything in a permission set except a few items, you’d have to copy the entire permission set and manually remove entries. This was tedious, error-prone, and worst of all — when Microsoft updated their permission sets with new objects, your custom copy would miss those additions, potentially breaking security or functionality.

With exclude permissions, the approach is fundamentally different:

  1. Include Microsoft’s standard permission set (e.g., D365 Business Full Access) in a new custom permission set.
  2. Exclude only the specific objects you want to restrict.
  3. Assign your custom permission set to the user instead of the original.

This way, whenever Microsoft updates their permission set with new pages or reports, your users automatically get those additions — while your exclusions remain in effect.

Step-by-Step Demo: Excluding Pages

Erik demonstrates with two browser windows side by side — one logged in as “Erik 1” (a regular user) and one as an admin who can edit permissions.

Creating the Custom Permission Set

First, Erik navigates to Permission Sets and creates a new one called “YouTube Demo.” The process is:

  1. Open the new permission set’s permissions.
  2. Add an Include entry for the D365 Business Full Access permission set. This brings in everything from Microsoft’s standard set.
  3. Add Exclude entries for specific pages:
    • Page 16 — Chart of Accounts
    • Page 17 — General Ledger Entries
    • Page 19 — (another GL-related page)
    • Page 20 — General Journal

After adding the exclusions, the permission set shows the included Microsoft set as “Partial” rather than fully enforced — indicating that exclusions are active.

Assigning the Permission Set

On the user card for Erik 1, Erik removes the original D365 Business Full Access permission set and replaces it with the new “YouTube Demo” set. After refreshing the session, searching for “Chart of Accounts” from Erik 1’s perspective confirms that the page is no longer accessible.

Erik also notes an important detail: if there’s a related report (like a Chart of Accounts report) that you also want to restrict, you need to exclude that separately. The exclusion is object-specific — removing a page doesn’t automatically remove related reports.

Excluding Table Data Operations

The power of exclude permissions extends beyond pages. Erik demonstrates excluding specific table data operations on the Customer table (Table 18):

  • Exclude Insert, Modify, and Delete on the Customer table.
  • Leave Read access intact.

The result: Erik 1 can still view customers but cannot create new ones (the “New” button disappears), cannot edit existing records, and cannot delete them. This is exception-based handling — rather than explicitly granting only read access, you take away what you don’t need from the full permission set.

Erik does note a minor UI issue: the Delete action in the customer card may still appear clickable even when the user lacks delete permission — something he calls out as a potential Microsoft bug.

Important Rules to Remember

  • Exclude permissions must live inside a permission set. You cannot apply exclusions directly on a user record. You must create a permission set that includes the base set and defines the exclusions, then assign that permission set to the user.
  • You can nest permission sets. Your custom permission set with inclusions and exclusions can itself be included in other permission sets, allowing for hierarchical security models.
  • The original Microsoft permission sets remain untouched. This is the key advantage — Microsoft can update their sets, and your users benefit from those updates automatically while your exclusions stay in effect.

Bonus: A User Management Page

While this video focused on the exclude permissions concept rather than AL development, Erik did provide a handy custom page that gives administrators a consolidated view of user setup across multiple areas. Here’s the source code for a “User Management” list page that shows at a glance whether each user has been configured in various subsystems:

page 50100 "User Management"
{
    ApplicationArea = All;
    Caption = 'User Management';
    PageType = List;
    SourceTable = User;
    UsageCategory = Administration;
    Editable = false;

    layout
    {
        area(Content)
        {
            repeater(General)
            {
                field("User Name"; Rec."User Name")
                {
                    Width = 5;
                }
                field("Full Name"; Rec."Full Name")
                {
                    Width = 10;
                }
                field(State; Rec.State)
                {
                    Width = 5;
                }
                field(SettingsCtl; Settings)
                {
                    Editable = false;
                    Caption = 'Settings';
                    Width = 1;
                    trigger OnDrillDown()
                    var
                        UserSettings: Record "User Personalization";
                    begin
                        UserSettings.Setrange("User SID", Rec."User Security ID");
                        Page.RunModal(Page::"User Personalization", UserSettings);
                        CurrPage.Update();
                    end;
                }
                field(AccountingSetupCtl; AccountingSetup)
                {
                    Editable = false;
                    Caption = 'Accounting';
                    Width = 1;
                    trigger OnDrillDown()
                    var
                        U: Record "User Setup";
                    begin
                        U.Setrange("User ID", Rec."User Name");
                        Page.RunModal(Page::"User Setup", U);
                        CurrPage.Update();
                    end;
                }
                field(ApprovalCtl; Approval)
                {
                    Editable = false;
                    Caption = 'Approval';
                    Width = 1;
                    trigger OnDrillDown()
                    var
                        U: Record "User Setup";
                    begin
                        U.Setrange("User ID", Rec."User Name");
                        Page.RunModal(Page::"Approval User Setup", U);
                        CurrPage.Update();
                    end;
                }
                field(WarehouseCtl; Warehouse)
                {
                    Editable = false;
                    Caption = 'Warehouse';
                    Width = 1;
                    trigger OnDrillDown()
                    var
                        U: Record "Warehouse Employee";
                    begin
                        U.Setrange("User ID", Rec."User Name");
                        Page.RunModal(Page::"Warehouse Employees", U);
                        CurrPage.Update();
                    end;
                }
                field(FACtl; FA)
                {
                    Editable = false;
                    Caption = 'Fixed Assets';
                    Width = 1;
                    trigger OnDrillDown()
                    var
                        U: Record "FA Journal Setup";
                    begin
                        U.Setrange("User ID", Rec."User Name");
                        Page.RunModal(Page::"FA Journal Setup", U);
                        CurrPage.Update();
                    end;
                }
                field(ResourceCtl; Resource)
                {
                    Editable = false;
                    Caption = 'Resource';
                    Width = 1;
                    trigger OnDrillDown()
                    var
                        U: Record Resource;
                    begin
                        U.Setrange("No.", Rec."User Name");
                        Page.RunModal(Page::"Resource Card", U);
                        CurrPage.Update();
                    end;
                }
                field(EmployeeCtl; Employee)
                {
                    Editable = false;
                    Caption = 'Employee';
                    Width = 1;
                    trigger OnDrillDown()
                    var
                        U: Record Employee;
                    begin
                        U.Setrange("No.", Rec."User Name");
                        Page.RunModal(Page::"Employee Card", U);
                        CurrPage.Update();
                    end;
                }
            }
        }
    }

    trigger OnAfterGetRecord()
    begin
        UpdateGlobals();
    end;

    trigger OnAfterGetCurrRecord()
    begin
        UpdateGlobals();
    end;

    local procedure UpdateGlobals()
    var
        UserSettings: Record "User Personalization";
        UserSetup: Record "User Setup";
        WarehouseEmployee: Record "Warehouse Employee";
        FASetup: Record "FA Journal Setup";
        ResourceRec: Record Resource;
        EmployeeRec: Record Employee;
    begin
        UserSettings.Setrange("User SID", Rec."User Security ID");
        if UserSettings.IsEmpty() then
            Settings := '❌'
        else
            Settings := '✅';

        UserSetup.Setrange("User ID", Rec."User Name");
        If UserSetup.IsEmpty() then
            AccountingSetup := '❌'
        else
            AccountingSetup := '✅';

        Approval := '❌';
        If UserSetup.FindFirst() then
            if (UserSetup."Sales Amount Approval Limit" <> 0) or
               (UserSetup."Purchase Amount Approval Limit" <> 0) or
               (UserSetup."Unlimited Sales Approval") or
               (UserSetup."Unlimited Purchase Approval") or
               (UserSetup."Unlimited Request Approval") or
               (UserSetup."Approver ID" <> '') then
                Approval := '✅';

        WarehouseEmployee.Setrange("User ID", Rec."User Name");
        if WarehouseEmployee.IsEmpty() then
            Warehouse := '❌'
        else
            Warehouse := '✅';

        FASetup.Setrange("User ID", Rec."User Name");
        if FASetup.IsEmpty() then
            FA := '❌'
        else
            FA := '✅';

        ResourceRec.Setrange("No.", Rec."User Name");
        if ResourceRec.IsEmpty() then
            Resource := '❌'
        else
            Resource := '✅';

        EmployeeRec.Setrange("No.", Rec."User Name");
        if EmployeeRec.IsEmpty() then
            Employee := '❌'
        else
            Employee := '✅';
    end;

    var
        Settings: Text;
        AccountingSetup: Text;
        Approval: Text;
        Warehouse: Text;
        FA: Text;
        Resource: Text;
        Employee: Text;
}

This page uses emoji indicators (✅ and ❌) to show at a glance whether each user has been configured in User Personalization, User Setup (Accounting), Approval Setup, Warehouse Employee, FA Journal Setup, Resource, and Employee. Each column is drillable, opening the relevant setup page filtered to the selected user. It’s a practical administration tool that complements proper permission management.

Conclusion

Exclude permissions in Business Central are a powerful and elegant approach to security management. Instead of the old method of copying and painstakingly editing permission sets, you can now take Microsoft’s standard sets as-is, include them in a custom set, and simply exclude the specific objects or operations you want to restrict. This keeps your security configuration clean, maintainable, and future-proof as Microsoft continues to update their standard permission sets. Whether you’re a consultant, administrator, or developer, this is a feature you should absolutely have in your toolkit.