Codeunit 9018 Azure AD Plan Impl. in 24

App
System Application
Namespace
System.Azure.Identity

Procedures, 26Events, 2

Versions171819202122232425262728latest

Source242526272829

Source in 24

src/System Application/App/Azure AD Plan/src/AzureADPlanImpl.Codeunit.al898 lines, Copyright (c) Microsoft Corporation. MIT

// ------------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
// ------------------------------------------------------------------------------------------------

namespace System.Azure.Identity;

using System;
using System.Security.User;
using System.Environment;
using System.Environment.Configuration;
using System.Security.AccessControl;

codeunit 9018 "Azure AD Plan Impl."
{
    Access = Internal;
    InherentEntitlements = X;
    InherentPermissions = X;

    Permissions = tabledata Company = r,
                  tabledata Plan = rimd,
                  tabledata User = r,
                  tabledata "User Personalization" = rm,
                  tabledata "User Plan" = rimd;

    var
        UserLoginTimeTracker: Codeunit "User Login Time Tracker";
        AzureADGraph: Codeunit "Azure AD Graph";
        AzureADGraphUser: Codeunit "Azure AD Graph User";
        UserSetupCategoryTxt: Label 'User Setup', Locked = true;
        DeviceGroupNameTxt: Label 'Dynamics 365 Business Central Device Users', Locked = true;
        DevicePlanFoundMsg: Label 'Device plan %1 found for user with authentication object ID %2', Locked = true;
        NotBCUserMsg: Label 'User with authentication object ID %1 is not a Business Central user', Locked = true;
        UserPlanAssignedMsg: Label 'User with authentication object ID %1 is assigned plan %2', Locked = true;
        GettingUpdatesTxt: Label 'Fetching graph updates for user with authentication object ID: %1', Locked = true;
        SkippingUpdatesTxt: Label 'User is not part of security group. Skipping updates for user with authentication object ID: %1', Locked = true;
        PlanNotEnabledMsg: Label 'Plan is assigned to user but it is not enabled. Plan ID: %1', Locked = true;
        NotBCPlanAssignedMsg: Label 'Plan is assigned to user but it is not recognized as a BC plan. Plan ID: %1', Locked = true;
        DeviceUserWithBcPlanMsg: Label 'User with authentication object ID %1 is a member of the Device group, but also has Business Central plans assigned. The Device plan will not be assigned to this user.', Locked = true;
        DeviceUserCannotBeFirstUserErr: Label 'The device user cannot be the first user to log into the system.';
        UserGotPlanTxt: Label 'The Graph User with the authentication object ID %1 has a plan with ID %2 named %3.', Comment = '%1 = Authentication email (email); %2 = subscription plan ID (guid); %3 = Plan name (tex1t)', Locked = true;
        PlansDifferentCheckTxt: Label 'Checking if plans different for graph user with authentication object ID %1 and BC user with security ID %2.', Comment = '%1 = Authentication email (email); %2 = user security ID (guid)', Locked = true;
        PlanCountDifferentTxt: Label 'The count of plans in BC is %1 and count of plans in Graph is %2.', Locked = true;
        UserNotInUserTableTxt: Label 'The user is not present in the User table. Security ID: %1.', Locked = true;
        AzureGraphUserNotFoundTxt: Label 'Could not retrieve an Azure Graph user for User Security ID: %1.', Locked = true;
        AzurePlanRoleCenterFoundTxt: Label 'Found role center %1 for user %2 from Azure Plan.', Locked = true;
        NoPlanHasRoleCenterTxt: Label 'There is no plan for the user with a valid Role Center ID.', Locked = true;
        GraphUserHasExtraPlanTxt: Label 'Graph user has plan with ID %1 and named %2 that BC user does not have.', Locked = true, Comment = '%1 = Plan ID (guid); %2 = Plan name';
        MixedPlansExistTxt: Label 'Check for mixed plans. Basic plan exists: %1, Essentials plan exists: %2; Premium plan exists: %3.', Locked = true;
        UserDoesNotExistTxt: Label 'User with user SID %1 does not exist or does not have an authentication object ID', Locked = true;
        UsersWithMixedPlansTxt: Label 'Check for mixed plans. Authentication object ID for the first conflicting user: [%1]; second conflicting user [%2].', Locked = true;
        CheckingForMixedPlansTxt: Label 'Checking for mixed plans...', Locked = true;
        BasicPlanNameTxt: Label 'D365 Business Central Basic Financials', Locked = true;
        EssentialsPlanNameTxt: Label 'Dynamics 365 Business Central Essential', Locked = true;
        PremiumPlanNameTxt: Label 'Dynamics 365 Business Central Premium', Locked = true;
        ClearPersonalizationTxt: Label 'Clear company in User Personalization', Locked = true;
        NoDelegatedRoleTxt: Label 'User does not have a delegated role (e.g. Delegated Admin or Delegated Helpdesk)', Locked = true;
        AssigningPlanForDelegatedRoleTxt: Label 'Assigning plan %1 for a user with a delegated role (e.g. Delegated Admin or Delegated Helpdesk)', Comment = '%1 = the plan ID', Locked = true;
        RemovedSUPERFromUserTxt: Label 'Removed SUPER from the current user', Locked = true;

    [NonDebuggable]
    procedure IsPlanAssigned(PlanGUID: Guid): Boolean
    var
        UsersInPlans: Query "Users in Plans";
    begin
        UsersInPlans.SetRange(User_State, UsersInPlans.User_State::Enabled);
        UsersInPlans.SetRange(Plan_ID, PlanGUID);

        if UsersInPlans.Open() then
            exit(UsersInPlans.Read());
    end;

    [NonDebuggable]
    procedure IsPlanAssignedToUser(PlanGUID: Guid): Boolean
    begin
        exit(IsPlanAssignedToUser(PlanGUID, UserSecurityId()));
    end;

    [NonDebuggable]
    procedure IsPlanAssignedToUser(PlanGUID: Guid; UserGUID: Guid): Boolean
    var
        UserPlan: Record "User Plan";
    begin
        UserPlan.SetRange("User Security ID", UserGUID);
        UserPlan.SetRange("Plan ID", PlanGUID);
        exit(not UserPlan.IsEmpty());
    end;

    [NonDebuggable]
    procedure IsGraphUserEntitledFromServicePlan(var GraphUserInfo: DotNet UserInfo): Boolean
    var
        AssignedPlan: DotNet ServicePlanInfo;
        ServicePlanIdValue: Variant;
    begin
        if not IsNull(GraphUserInfo.AssignedPlans()) then
            foreach AssignedPlan in GraphUserInfo.AssignedPlans() do
                if Format(AssignedPlan.CapabilityStatus()) = 'Enabled' then begin
                    ServicePlanIdValue := AssignedPlan.ServicePlanId();
                    if IsBCServicePlan(ServicePlanIdValue) then
                        exit(true);
                end;

        if IsDeviceRole(GraphUserInfo) then
            exit(true);

        exit(false);
    end;

    [NonDebuggable]
    procedure UpdateUserPlans(UserSecurityId: Guid; var GraphUserInfo: DotNet UserInfo; AppendPermissionsOnNewPlan: Boolean; RemovePermissionsOnDeletePlan: Boolean)
    var
        TempPlan: Record Plan temporary;
        UserPlan: Record "User Plan";
        HasUserBeenSetupBefore: Boolean;
    begin
        GetGraphUserPlans(TempPlan, GraphUserInfo);

        // Has the user been setup earlier?
        UserPlan.SetRange("User Security ID", UserSecurityId);
        HasUserBeenSetupBefore := not (UserPlan.IsEmpty() and (not UserLoginTimeTracker.UserLoggedInEnvironment(UserSecurityId)));

        // Have any plans been removed from this user in O365, since last time he logged-in to NAV?
        RemoveUnassignedUserPlans(TempPlan, UserSecurityId, RemovePermissionsOnDeletePlan);

        // Have any plans been added to this user in O365, since last time he logged-in to NAV?
        AddNewlyAssignedUserPlans(TempPlan, UserSecurityId, HasUserBeenSetupBefore, AppendPermissionsOnNewPlan);
    end;

    [NonDebuggable]
    procedure UpdateUserPlans(UserSecurityId: Guid; AppendPermissionsOnNewPlan: Boolean; RemovePermissionsOnDeletePlan: Boolean; RemovePlansOnDeleteUser: Boolean)
    var
        TempDummyPlan: Record Plan temporary;
        GraphUserInfo: DotNet UserInfo;
    begin
        if AzureADGraphUser.GetGraphUser(UserSecurityId, true, GraphUserInfo) then
            UpdateUserPlans(UserSecurityId, GraphUserInfo, AppendPermissionsOnNewPlan, RemovePermissionsOnDeletePlan)
        else
            if RemovePlansOnDeleteUser then
                RemoveUnassignedUserPlans(TempDummyPlan, UserSecurityId, RemovePermissionsOnDeletePlan);
    end;

    [NonDebuggable]
    procedure UpdateUserPlans()
    var
        User: Record User;
        UserSelection: Codeunit "User Selection";
    begin
        UserSelection.FilterSystemUserAndAADGroupUsers(User);
        User.SetFilter("Windows Security ID", '%1', '');

        if not User.FindSet() then
            exit;

        repeat
            UpdateUserPlans(User."User Security ID", true, true, false);
        until User.Next() = 0;
    end;

    [NonDebuggable]
    procedure RefreshUserPlanAssignments(UserSecurityID: Guid)
    var
        User: Record User;
        UsersInPlan: Query "Users in Plans";
        GraphUserInfo: DotNet UserInfo;
        UserPlanExists: Boolean;
    begin
        if not User.Get(UserSecurityID) then
            exit;

        if not AzureADGraphUser.GetGraphUser(UserSecurityID, GraphUserInfo) then
            exit;

        // Is this the first user being setup
        if UsersInPlan.Open() then
            if UsersInPlan.Read() then
                UserPlanExists := true;

        if not UserPlanExists then
            if IsDeviceRole(GraphUserInfo) then
                Error(DeviceUserCannotBeFirstUserErr);

        UpdateUserFromAzureGraph(User, GraphUserInfo);
        UpdateUserPlans(User."User Security ID", GraphUserInfo, true, true);
    end;

    [TryFunction]
    [NonDebuggable]
    procedure TryGetAzureUserPlanRoleCenterId(var RoleCenterID: Integer; UserSecurityID: Guid)
    begin
        RoleCenterID := GetAzureUserPlanRoleCenterId(UserSecurityID);
    end;

    [NonDebuggable]
    procedure DoPlansExist(): Boolean
    var
        Plan: Record Plan;
    begin
        exit(not Plan.IsEmpty());
    end;

    [NonDebuggable]
    procedure DoUserPlansExist(): Boolean
    var
        UserPlan: Record "User Plan";
    begin
        exit(not UserPlan.IsEmpty());
    end;

    [NonDebuggable]
    procedure DoesPlanExist(PlanGUID: Guid): Boolean
    var
        Plan: Record Plan;
    begin
        exit(Plan.Get(PlanGUID));
    end;

    [NonDebuggable]
    procedure DoesUserHavePlans(UserSecurityId: Guid): Boolean
    var
        UserPlan: Record "User Plan";
    begin
        UserPlan.SetRange("User Security ID", UserSecurityId);
        exit(not UserPlan.IsEmpty());
    end;

    [NonDebuggable]
    procedure GetAvailablePlansCount(): Integer
    var
        Plan: Record Plan;
    begin
        exit(Plan.Count());
    end;

    [NonDebuggable]
    procedure GetAllPlanIds(): List of [Guid]
    var
        Plan: Record Plan;
        PlanIDs: List of [Guid];
    begin
        if Plan.FindSet() then
            repeat
                PlanIDs.Add(Plan."Plan ID");
            until Plan.Next() = 0;
        exit(PlanIDs);
    end;

    procedure GetUserPlanExperience(): Enum "User Plan Experience"
    var
        PlanIds: Codeunit "Plan Ids";
        UserPlanExperience: Enum "User Plan Experience";
    begin
        if UserHasPlan(UserSecurityId(), PlanIds.GetPremiumPlanId()) then
            exit(UserPlanExperience::Premium);

        if UserHasPlan(UserSecurityId(), PlanIds.GetEssentialPlanId()) then
            exit(UserPlanExperience::Essentials);

        if UserHasPlan(UserSecurityId(), PlanIds.GetBasicPlanId()) then
            exit(UserPlanExperience::Basic);

        exit(UserPlanExperience::Other);
    end;

    procedure CheckMixedPlansExist(): Boolean
    var
        DummyDictionary: Dictionary of [Text, List of [Text]];
    begin
        exit(CheckMixedPlansExist(DummyDictionary));
    end;

    procedure CheckMixedPlansExist(PlanNamesPerUserFromGraph: Dictionary of [Text, List of [Text]]): Boolean
    begin
        if not ShouldCheckMixedPlans() then
            exit(false);

        exit(MixedPlansExist(PlanNamesPerUserFromGraph));
    end;

    procedure MixedPlansExist(): Boolean
    var
        EmptyDictionary: Dictionary of [Text, List of [Text]];
    begin
        exit(MixedPlansExist(EmptyDictionary));
    end;

    procedure MixedPlansExist(PlanNamesPerUserFromGraph: Dictionary of [Text, List of [Text]]): Boolean
    var
        PlanIds: Codeunit "Plan Ids";
        UsersInPlans: Query "Users in Plans";
        PlanNamesPerUser: Dictionary of [Text, List of [Text]];
        AuthenticationObjectIDs: List of [Text];
        PlanNames: List of [Text];
        UserAuthenticationObjectId: Text;
        CurrentUserPlanList: List of [Text];
        UserAuthenticationEmailFirstConflicting: Text;
        UserAuthenticationEmailSecondConflicting: Text;
        FirstConflictingPlanName: Text;
        SecondConflictingPlanName: Text;
        BasicPlanExists: Boolean;
        EssentialsPlanExists: Boolean;
        PremiumPlanExists: Boolean;
    begin
        Session.LogMessage('0000BPB', CheckingForMixedPlansTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', UserSetupCategoryTxt);

        // Get content of the User plan table into a new Dictionary
        UsersInPlans.SetRange(User_State, UsersInPlans.User_State::Enabled);
        if UsersInPlans.Open() then
            while UsersInPlans.Read() do
                if AzureADGraphUser.TryGetUserAuthenticationObjectId(UsersInPlans.User_Security_ID, UserAuthenticationObjectId) then begin
                    if UserAuthenticationObjectId <> '' then begin
                        Clear(CurrentUserPlanList);
                        if PlanNamesPerUser.ContainsKey(UserAuthenticationObjectId) then
                            CurrentUserPlanList := PlanNamesPerUser.Get(UserAuthenticationObjectId);
                        CurrentUserPlanList.Add(UsersInPlans.Plan_Name);
                        PlanNamesPerUser.Set(UserAuthenticationObjectId, CurrentUserPlanList);
                    end;
                end else
                    Session.LogMessage('0000CMW', StrSubstNo(UserDoesNotExistTxt, UsersInPlans.User_Security_ID), Verbosity::Verbose, DataClassification::EndUserPseudonymousIdentifiers, TelemetryScope::ExtensionPublisher, 'Category', 'UserSetupCategoryTxt');

        // update the dictionary with the values from input
        foreach UserAuthenticationObjectId in PlanNamesPerUserFromGraph.Keys do
            PlanNamesPerUser.Set(UserAuthenticationObjectId, PlanNamesPerUserFromGraph.Get(UserAuthenticationObjectId));

        BasicPlanExists := PlansExist(PlanNamesPerUser, PlanIds.GetBasicPlanId(), AuthenticationObjectIDs, PlanNames);
        EssentialsPlanExists := PlansExist(PlanNamesPerUser, PlanIds.GetEssentialPlanId(), AuthenticationObjectIDs, PlanNames);
        PremiumPlanExists := PlansExist(PlanNamesPerUser, PlanIds.GetPremiumPlanId(), AuthenticationObjectIDs, PlanNames);

        Session.LogMessage('0000BPC', StrSubstNo(MixedPlansExistTxt, BasicPlanExists, EssentialsPlanExists, PremiumPlanExists), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', UserSetupCategoryTxt);

        if PlanNames.Count() > 1 then begin
            UserAuthenticationEmailFirstConflicting := GetAuthenticationEmailFromAuthenticationObjectID(AuthenticationObjectIDs.Get(1));
            UserAuthenticationEmailSecondConflicting := GetAuthenticationEmailFromAuthenticationObjectID(AuthenticationObjectIDs.Get(2));
            FirstConflictingPlanName := PlanNames.Get(1);
            SecondConflictingPlanName := PlanNames.Get(2);
            Session.LogMessage('0000BPD', StrSubstNo(UsersWithMixedPlansTxt, AuthenticationObjectIDs.Get(1), AuthenticationObjectIDs.Get(2)), Verbosity::Normal, DataClassification::EndUserPseudonymousIdentifiers, TelemetryScope::ExtensionPublisher, 'Category', UserSetupCategoryTxt);
            exit(true);
        end;
    end;

    local procedure ShouldCheckMixedPlans(): Boolean
    var
        Company: Record Company;
        EnvironmentInformation: Codeunit "Environment Information";
    begin
        if not EnvironmentInformation.IsSaaS() then
            exit(false);

        if not GuiAllowed() then
            exit(false);

        if Company.Get(CompanyName()) then
            if Company."Evaluation Company" then
                exit(false);

        if not DoPlansExist() then
            exit(false);

        if not DoUserPlansExist() then
            exit(false);

        exit(true);
    end;

    [NonDebuggable]
    local procedure GetAuthenticationEmailFromAuthenticationObjectID(UserAuthenticationObjectID: Text): Text
    var
        User: Record User;
        GraphUserInfo: DotNet UserInfo;
    begin
        if AzureADGraphUser.GetUser(UserAuthenticationObjectID, User) then
            exit(User."Authentication Email")
        else begin
            AzureADGraph.GetUserByObjectId(UserAuthenticationObjectID, GraphUserInfo);
            exit(AzureADGraphUser.GetAuthenticationEmail(GraphUserInfo));
        end;
    end;

    [NonDebuggable]
    local procedure GetPlanName(PlanId: Guid) PlanName: Text
    var
        PlanIds: Codeunit "Plan Ids";
    begin
        case PlanId of
            PlanIds.GetBasicPlanId():
                PlanName := BasicPlanNameTxt;
            PlanIds.GetEssentialPlanId():
                PlanName := EssentialsPlanNameTxt;
            PlanIds.GetPremiumPlanId():
                PlanName := PremiumPlanNameTxt;
        end;
    end;

    local procedure UserHasPlan(UserSecurityId: Guid; PlanId: Guid): Boolean
    var
        UserPlan: Record "User Plan";
    begin
        UserPlan.SetRange("User Security ID", UserSecurityId);
        UserPlan.SetRange("Plan ID", PlanId);
        exit(not UserPlan.IsEmpty());
    end;

    [NonDebuggable]
    local procedure PlansExist(var PlanNamesPerUser: Dictionary of [Text, List of [Text]]; PlanId: Guid; var AuthenticationObjectIDs: List of [Text]; var PlanNames: List of [Text]): Boolean
    var
        Plan: Record Plan;
        CurrentAuthenticationObjectId: Text;
        PlanNameList: List of [Text];
        PlanName: Text;
    begin
        if Plan.Get(PlanId) then
            PlanName := Plan.Name
        else
            PlanName := GetPlanName(PlanId);
        foreach CurrentAuthenticationObjectId in PlanNamesPerUser.Keys() do begin
            PlanNameList := PlanNamesPerUser.Get(CurrentAuthenticationObjectId);
            if PlanNameList.Contains(PlanName) then begin
                AuthenticationObjectIDs.Add(CurrentAuthenticationObjectId);
                PlanNames.Add(PlanName);
                exit(true);
            end;
        end;
    end;

    [NonDebuggable]
    local procedure RemoveUnassignedUserPlans(var TempPlan: Record Plan temporary; UserSecurityID: Guid; RemovePermissionsOnDeletePlan: Boolean)
    var
        NavUserPlan: Record "User Plan";
        TempNavUserPlan: Record "User Plan" temporary;
#if not CLEAN22
        AzureADPlan: Codeunit "Azure AD Plan";
#endif
        PlanConfiguration: Codeunit "Plan Configuration";
        IsCustomized: Boolean;
    begin
        // Have any plans been removed from this user in O365, since last time he logged-in to Business Central?

        // Get all plans assigned to the user, in NAV
        NavUserPlan.SetRange("User Security ID", UserSecurityID);
        if not NavUserPlan.FindSet() then
            exit;

        repeat
            TempNavUserPlan.Copy(NavUserPlan, false);
            TempNavUserPlan.Insert();
        until NavUserPlan.Next() = 0;

        // Get all plans assigned to the user in Office
        if TempPlan.FindSet() then
            // And remove them from the list of plans assigned to the user
            repeat
                TempNavUserPlan.SetRange("Plan ID", TempPlan."Plan ID");
                if not TempNavUserPlan.IsEmpty() then
                    TempNavUserPlan.DeleteAll();
            until TempPlan.Next() = 0;

        // if any plans belong to the user in NAV, but not in Office, de-assign them
        TempNavUserPlan.SetRange("Plan ID");
        if TempNavUserPlan.FindSet() then
            repeat
                NavUserPlan.SetRange("Plan ID", TempNavUserPlan."Plan ID");
                if NavUserPlan.FindFirst() then begin
                    NavUserPlan.LockTable();
                    NavUserPlan.Delete();
                    if RemovePermissionsOnDeletePlan then begin
#if not CLEAN22
#pragma warning disable AL0432
                        AzureADPlan.OnRemoveUserGroupsForUserAndPlan(NavUserPlan."Plan ID", NavUserPlan."User Security ID");
#pragma warning restore AL0432
#endif
                        IsCustomized := PlanConfiguration.IsCustomized(NavUserPlan."Plan ID");
                        if IsCustomized then
                            PlanConfiguration.RemoveCustomPermissionsFromUser(NavUserPlan."Plan ID", UserSecurityID)
                        else
                            PlanConfiguration.RemoveDefaultPermissionsFromUser(NavUserPlan."Plan ID", UserSecurityID);
                    end;

                    Commit(); // Finalize the transaction. Else any further error can rollback and create elevation of privilege
                end;
            until TempNavUserPlan.Next() = 0;
    end;

    [NonDebuggable]
    local procedure GetGraphUserPlans(var TempPlan: Record Plan temporary; var GraphUserInfo: DotNet UserInfo)
    var
        PlanIds: Codeunit "Plan Ids";
        AssignedPlan: DotNet ServicePlanInfo;
        DirectoryRole: DotNet RoleInfo;
        ServicePlanIdValue: Variant;
        DevicesPlanId: Guid;
        DevicesPlanName: Text;
    begin
        Session.LogMessage('0000NMI', StrSubstNo(GettingUpdatesTxt, Format(GraphUserInfo.ObjectId())), Verbosity::Normal, DataClassification::EndUserPseudonymousIdentifiers, TelemetryScope::ExtensionPublisher, 'Category', UserSetupCategoryTxt);

        TempPlan.Reset();
        TempPlan.DeleteAll();

        // Do not consider plans of non-admin users who are not members of the environment security group
        if AzureADGraph.IsEnvironmentSecurityGroupDefined() then
            if (not AzureADGraph.IsMemberOfGroupWithId(AzureADGraph.GetEnvironmentSecurityGroupId(), GraphUserInfo)) then
                if not IsInternalAdmin(GraphUserInfo) then begin
                    Session.LogMessage('0000NMJ', StrSubstNo(SkippingUpdatesTxt, Format(GraphUserInfo.ObjectId())), Verbosity::Normal, DataClassification::EndUserPseudonymousIdentifiers, TelemetryScope::ExtensionPublisher, 'Category', UserSetupCategoryTxt);
                    exit;
                end;

        // Loop through assigned Microsoft Entra Plans
        if not IsNull(GraphUserInfo.AssignedPlans()) then
            foreach AssignedPlan in GraphUserInfo.AssignedPlans() do begin
                ServicePlanIdValue := AssignedPlan.ServicePlanId();

                if Format(AssignedPlan.CapabilityStatus()) = 'Enabled' then begin
                    if IsBCServicePlan(ServicePlanIdValue) then begin
                        AddToTempPlan(ServicePlanIdValue, Format(AssignedPlan.ServicePlanName()), TempPlan);
                        Session.LogMessage('00009KY', StrSubstNo(UserPlanAssignedMsg, Format(GraphUserInfo.ObjectId()), Format(ServicePlanIdValue)), Verbosity::Normal, DataClassification::EndUserPseudonymousIdentifiers, TelemetryScope::ExtensionPublisher, 'Category', UserSetupCategoryTxt);
                    end else
                        Session.LogMessage('0000I94', StrSubstNo(NotBCPlanAssignedMsg, Format(ServicePlanIdValue)), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', UserSetupCategoryTxt);
                end else
                    Session.LogMessage('0000I95', StrSubstNo(PlanNotEnabledMsg, Format(ServicePlanIdValue)), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', UserSetupCategoryTxt);
            end;

        // Loop through Microsoft Entra Roles
        if not IsNull(GraphUserInfo.Roles()) then
            foreach DirectoryRole in GraphUserInfo.Roles() do
                if IsBCServicePlan(DirectoryRole.RoleTemplateId()) then begin
                    AddToTempPlan(Format(DirectoryRole.RoleTemplateId()), Format(DirectoryRole.DisplayName()), TempPlan);
                    Session.LogMessage('00009L0', StrSubstNo(UserPlanAssignedMsg, Format(GraphUserInfo.ObjectId()), Format(DirectoryRole.RoleTemplateId())), Verbosity::Normal, DataClassification::EndUserPseudonymousIdentifiers, TelemetryScope::ExtensionPublisher, 'Category', UserSetupCategoryTxt);
                end;

        // Check if the user is a member of the Device group
        if IsDeviceRole(GraphUserInfo) then begin
            // Only assign the device plan if the user doesn't have any other plans (except possibly Internal Admin or M365 Collaboration)
            TempPlan.SetFilter("Plan ID", '<>%1&<>%2&<>%3&<>%4', PlanIds.GetGlobalAdminPlanId(), PlanIds.GetD365AdminPlanId(), PlanIds.GetBCAdminPlanId(), PlanIds.GetMicrosoft365PlanId());

            if TempPlan.IsEmpty() then begin
                // Remove the Internal Admin and M365 Collaboration plans, if assigned
                TempPlan.Reset();
                TempPlan.DeleteAll();

                GetDevicesPlanInfo(DevicesPlanId, DevicesPlanName);
                Session.LogMessage('00009L6', StrSubstNo(DevicePlanFoundMsg, DevicesPlanName, Format(GraphUserInfo.ObjectId())), Verbosity::Normal, DataClassification::EndUserPseudonymousIdentifiers, TelemetryScope::ExtensionPublisher, 'Category', UserSetupCategoryTxt);
                AddToTempPlan(DevicesPlanId, DevicesPlanName, TempPlan);
            end else begin
                Session.LogMessage('0000K5Z', StrSubstNo(DeviceUserWithBcPlanMsg, Format(GraphUserInfo.ObjectId())), Verbosity::Normal, DataClassification::EndUserPseudonymousIdentifiers, TelemetryScope::ExtensionPublisher, 'Category', UserSetupCategoryTxt);
                TempPlan.Reset();
            end;
        end;

        if TempPlan.IsEmpty() then
            Session.LogMessage('00009L7', StrSubstNo(NotBCUserMsg, Format(GraphUserInfo.ObjectId())), Verbosity::Normal, DataClassification::EndUserPseudonymousIdentifiers, TelemetryScope::ExtensionPublisher, 'Category', UserSetupCategoryTxt);
    end;

    [NonDebuggable]
    local procedure IsDeviceRole(var GraphUserInfo: DotNet UserInfo): Boolean
    begin
        exit(AzureADGraph.IsGroupMember(DeviceGroupNameTxt, GraphUserInfo));
    end;

    [NonDebuggable]
    local procedure IsInternalAdmin(var GraphUserInfo: DotNet UserInfo): Boolean
    var
        PlanIds: Codeunit "Plan Ids";
        DirectoryRole: DotNet RoleInfo;
        RoleId: Guid;
    begin
        if not IsNull(GraphUserInfo.Roles()) then
            foreach DirectoryRole in GraphUserInfo.Roles() do begin
                RoleId := DirectoryRole.RoleTemplateId();
                if RoleId in [PlanIds.GetGlobalAdminPlanId(), PlanIds.GetD365AdminPlanId(), PlanIds.GetBCAdminPlanId()] then
                    exit(true);
            end;

        exit(false);
    end;

    [NonDebuggable]
    local procedure GetDevicesPlanInfo(var PlanId: Guid; var PlanName: Text)
    var
        Plan: Record Plan;
        PlanIds: Codeunit "Plan Ids";
    begin
        PlanId := PlanIds.GetDevicePlanId();
        Plan.Get(PlanIds.GetDevicePlanId());
        PlanName := Plan.Name;
    end;

    [NonDebuggable]
    local procedure InsertFromTempPlan(TempPlan: Record Plan temporary)
    var
        Plan: Record Plan;
    begin
        if not Plan.Get(TempPlan."Plan ID") then begin
            Plan.Copy(TempPlan);
            Plan.Insert();
        end;
    end;

    [NonDebuggable]
    local procedure UpdateUserFromAzureGraph(var User: Record User; var GraphUserInfo: DotNet UserInfo): Boolean
    var
        IsUserModified: Boolean;
    begin
        AzureADGraphUser.GetGraphUser(User."User Security ID", GraphUserInfo);
        IsUserModified := AzureADGraphUser.UpdateUserFromAzureGraph(User, GraphUserInfo);
        exit(IsUserModified);
    end;

    [NonDebuggable]
    procedure IsBCServicePlan(ServicePlanId: Guid): Boolean
    var
        Plan: Record Plan;
        PlanIds: Codeunit "Plan Ids";
        Skip: Boolean;
        IsPlanFound: Boolean;
    begin
        OnBeforeIsBcServicePlan(Skip);
        if Skip then
            exit(true);

        if IsNullGuid(ServicePlanId) then
            exit(false);

        IsPlanFound := Plan.Get(ServicePlanId);
        if (Plan."Plan ID" <> PlanIds.GetMicrosoft365PlanId()) then
            exit(IsPlanFound);

        // The current plan is M365 Collaboration. Only treat it as a BC plan if the environment switch is on.
        exit(AzureADGraph.IsM365CollaborationEnabled());
    end;

    [NonDebuggable]
    local procedure GetAzureUserPlanRoleCenterId(UserSecurityID: Guid): Integer
    var
        TempPlan: Record Plan temporary;
        User: Record User;
        GraphUserInfo: DotNet UserInfo;
    begin
        if not User.Get(UserSecurityID) then begin
            Session.LogMessage('0000DUD', StrSubstNo(UserNotInUserTableTxt, UserSecurityId()), Verbosity::Warning, DataClassification::EndUserPseudonymousIdentifiers, TelemetryScope::ExtensionPublisher, 'Category', UserSetupCategoryTxt);
            exit(0);
        end;

        if not AzureADGraphUser.GetGraphUser(UserSecurityID, GraphUserInfo) then begin
            Session.LogMessage('0000DUE', StrSubstNo(AzureGraphUserNotFoundTxt, UserSecurityId()), Verbosity::Warning, DataClassification::EndUserPseudonymousIdentifiers, TelemetryScope::ExtensionPublisher, 'Category', UserSetupCategoryTxt);
            exit(0);
        end;

        GetGraphUserPlans(TempPlan, GraphUserInfo);

        TempPlan.SetFilter("Role Center ID", '<>0');

        if not TempPlan.FindFirst() then begin
            Session.LogMessage('0000DUG', NoPlanHasRoleCenterTxt, Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', UserSetupCategoryTxt);
            exit(0);
        end;

        Session.LogMessage('0000DUC', StrSubstNo(AzurePlanRoleCenterFoundTxt, TempPlan."Role Center ID", UserSecurityId()), Verbosity::Normal,
            DataClassification::EndUserPseudonymousIdentifiers, TelemetryScope::ExtensionPublisher, 'Category', UserSetupCategoryTxt);

        exit(TempPlan."Role Center ID");
    end;

    [NonDebuggable]
    procedure AssignPlanToUserWithDelegatedRole(UserSID: Guid)
    var
        UserPlan: Record "User Plan";
        AzureADPlan: Codeunit "Azure AD Plan";
        PlanIds: Codeunit "Plan Ids";
        PlanConfigurationImpl: Codeunit "Plan Configuration Impl.";
        UserPermissions: Codeunit "User Permissions";
        UserGroupsAdded, ShouldRemoveSuper : Boolean;
        PlanId: Guid;
    begin
        case true of
            AzureADGraphUser.IsUserDelegatedAdmin():
                PlanId := PlanIds.GetDelegatedAdminPlanId();
            AzureADGraphUser.IsUserDelegatedHelpdesk():
                PlanId := PlanIds.GetHelpDeskPlanId();
            else begin
                Session.LogMessage('0000IC3', NoDelegatedRoleTxt, Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', UserSetupCategoryTxt);
                exit;
            end;
        end;

        Session.LogMessage('0000IC4', StrSubstNo(AssigningPlanForDelegatedRoleTxt, PlanId), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', UserSetupCategoryTxt);

        // Assign a plan for the user
        UserPlan.Init();
        UserPlan."Plan ID" := PlanId;
        UserPlan."User Security ID" := UserSID;
        UserPlan.Insert();

        // Assign user groups for the user
        AzureADPlan.OnUpdateUserAccessForSaaS(UserPlan."User Security ID", UserGroupsAdded);

        // Users with delegated roles (Delegated Admin or Delegated Helpdesk) has SUPER assigned by default
        // Remove SUPER from the user only if the plan permissions have been configured and that configuration does not contain SUPER
        ShouldRemoveSuper := PlanConfigurationImpl.IsCustomized(PlanId) and (not PlanConfigurationImpl.ConfigurationContainsSuper(PlanId));
        if UserGroupsAdded and ShouldRemoveSuper then begin
            if UserPermissions.RemoveSuperPermissions(UserSID) then
                Session.LogMessage('0000IC5', RemovedSUPERFromUserTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', UserSetupCategoryTxt);
            Commit();
        end;
    end;

    [NonDebuggable]
    local procedure AddNewlyAssignedUserPlans(var Plan: Record Plan; UserSecurityID: Guid; UserHadBeenSetupBefore: Boolean; AppendPermissionsOnNewPlan: Boolean)
    var
        UserPersonalization: Record "User Personalization";
        UserPlan: Record "User Plan";
        AzureADPlan: Codeunit "Azure AD Plan";
        UserPermissions: Codeunit "User Permissions";
        PlanConfigurationImpl: Codeunit "Plan Configuration Impl.";
        UserGroupsAdded, PlanConfigurationContainsSuper, IsPlanConfigurationCustomized, ShouldRemoveSuper : Boolean;
    begin
        // Have any plans been added to this user in O365, since last time he logged-in to BC?
        // For each plan assigned to the user in Office
        if Plan.FindSet() then
            repeat
                // Does this assignment exist in BC? If not, add it.
                UserPlan.LockTable();
                UserPlan.SetRange("Plan ID", Plan."Plan ID");
                UserPlan.SetRange("User Security ID", UserSecurityID);

                if UserPlan.IsEmpty() then begin
                    InsertFromTempPlan(Plan);
                    UserPlan.Init();
                    UserPlan."Plan ID" := Plan."Plan ID";
                    UserPlan."User Security ID" := UserSecurityID;
                    UserPlan.Insert();
                    // The SUPER role is replaced with O365 FULL ACCESS for new users.
                    // This happens only for users who are created from O365 (i.e. are added to plans)
                    if AppendPermissionsOnNewPlan then
                        AzureADPlan.OnUpdateUserAccessForSaaS(UserPlan."User Security ID", UserGroupsAdded);

                    PlanConfigurationContainsSuper := PlanConfigurationContainsSuper or PlanConfigurationImpl.ConfigurationContainsSuper(Plan."Plan ID");
                    IsPlanConfigurationCustomized := IsPlanConfigurationCustomized or PlanConfigurationImpl.IsCustomized(Plan."Plan ID");
                end;
            until Plan.Next() = 0;

        // Only remove SUPER if other permissions are granted (to avoid user lockout)
        if UserGroupsAdded and (not UserHadBeenSetupBefore) then begin
            if IsPlanConfigurationCustomized then begin
                // For newly-created users clear the company in case they are logged in to a company they don't have permissions for.
                // Clearing the company in user personalization will make platform pick the right company on next login.
                UserPersonalization.LockTable();
                if UserPersonalization.Get(UserSecurityID) and (UserPersonalization.Company <> '') then begin
                    UserPersonalization.Company := '';
                    if UserPersonalization.Modify() then
                        Session.LogMessage('0000GYC', ClearPersonalizationTxt, Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', UserSetupCategoryTxt);
                end;

                ShouldRemoveSuper := not PlanConfigurationContainsSuper
            end else
                ShouldRemoveSuper := not IsUserAdmin(UserSecurityID);

            if ShouldRemoveSuper then
                if UserPermissions.RemoveSuperPermissions(UserSecurityID) then
                    Session.LogMessage('0000IC6', RemovedSUPERFromUserTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', UserSetupCategoryTxt);
        end;

        Commit(); // Finalize the transaction. Else any further error can rollback and create elevation of privilege
    end;

    [NonDebuggable]
    local procedure AddToTempPlan(ServicePlanId: Guid; ServicePlanName: Text; var TempPlan: Record Plan temporary)
    var
        Plan: Record Plan;
        Handled: Boolean;
    begin
        if TempPlan.Get(ServicePlanId) then
            exit;

        if Plan.Get(ServicePlanId) then;

        TempPlan.Init();
        TempPlan."Plan ID" := ServicePlanId;
        TempPlan.Name := CopyStr(ServicePlanName, 1, MaxStrLen(TempPlan.Name));
        OnInitializeRoleCenter(TempPlan."Role Center ID", Handled);
        if not Handled then
            TempPlan."Role Center ID" := Plan."Role Center ID";
        TempPlan.Insert();

    end;

    [NonDebuggable]
    local procedure IsUserAdmin(SecurityID: Guid): Boolean
    var
        PlanIds: Codeunit "Plan Ids";
    begin
        exit(
            IsPlanAssignedToUser(PlanIds.GetGlobalAdminPlanId(), SecurityID)
            or IsPlanAssignedToUser(PlanIds.GetDelegatedAdminPlanId(), SecurityID)
            or IsPlanAssignedToUser(PlanIds.GetD365AdminPlanId(), SecurityID));
    end;

    [NonDebuggable]
    procedure GetPlanIDs(GraphUserInfo: DotNet UserInfo; var PlanIDs: List of [Guid])
    var
        TempPlan: Record Plan temporary;
    begin
        Clear(PlanIDs);
        GetGraphUserPlans(TempPlan, GraphUserInfo);
        if TempPlan.FindSet() then
            repeat
                PlanIDs.Add(TempPlan."Plan ID");
            until TempPlan.Next() = 0;
    end;

    [NonDebuggable]
    procedure GetPlanNames(GraphUserInfo: DotNet UserInfo; var PlanNames: List of [Text])
    var
        TempPlan: Record Plan temporary;
        Plan: Record Plan;
    begin
        Clear(PlanNames);
        GetGraphUserPlans(TempPlan, GraphUserInfo);
        if TempPlan.FindSet() then
            repeat
                // use the Business Central plan name instead of the Office Plan name, if possible.
                if Plan.Get(TempPlan."Plan ID") then begin
                    PlanNames.Add(Plan.Name);
                    Session.LogMessage('0000BK0', StrSubstNo(UserGotPlanTxt, GraphUserInfo.ObjectId(), Plan."Plan ID", Plan.Name), Verbosity::Verbose, DataClassification::EndUserPseudonymousIdentifiers, TelemetryScope::ExtensionPublisher, 'Category', UserSetupCategoryTxt);
                end else begin
                    PlanNames.Add(TempPlan.Name);
                    Session.LogMessage('0000BK1', StrSubstNo(UserGotPlanTxt, GraphUserInfo.ObjectId(), TempPlan."Plan ID", TempPlan.Name), Verbosity::Verbose, DataClassification::EndUserPseudonymousIdentifiers, TelemetryScope::ExtensionPublisher, 'Category', UserSetupCategoryTxt);
                end;

            until TempPlan.Next() = 0;
    end;

    [NonDebuggable]
    procedure GetPlanNames(UserSecID: Guid; var PlanNames: List of [Text])
    var
        UserPlan: Record "User Plan";
    begin
        Clear(PlanNames);
        UserPlan.SetRange("User Security ID", UserSecID);
        if UserPlan.FindSet() then
            repeat
                UserPlan.CalcFields("Plan Name");
                PlanNames.Add(UserPlan."Plan Name");
            until UserPlan.Next() = 0;
    end;

    [NonDebuggable]
    procedure CheckIfPlansDifferent(GraphUserInfo: DotNet UserInfo; UserSecID: Guid): Boolean
    var
        TempPlan: Record Plan temporary;
        UserPlan: Record "User Plan";
        Plan: Record Plan;
        UserPlanCount: Integer;
        TempPlanCount: Integer;
    begin
        Session.LogMessage('0000BK2', StrSubstNo(PlansDifferentCheckTxt, GraphUserInfo.ObjectId(), UserSecID), Verbosity::Normal, DataClassification::EndUserPseudonymousIdentifiers, TelemetryScope::ExtensionPublisher, 'Category', UserSetupCategoryTxt);

        GetGraphUserPlans(TempPlan, GraphUserInfo);

        UserPlan.SetRange("User Security ID", UserSecID);
        UserPlanCount := UserPlan.Count();
        TempPlanCount := TempPlan.Count();
        if UserPlanCount <> TempPlanCount then begin
            Session.LogMessage('0000BK3', StrSubstNo(PlanCountDifferentTxt, UserPlanCount, TempPlanCount), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', UserSetupCategoryTxt);
            exit(true);
        end;

        if TempPlan.FindSet() then
            repeat
                if not UserPlan.Get(TempPlan."Plan ID", UserSecID) then begin
                    if Plan.Get(TempPlan."Plan ID") then
                        Session.LogMessage('0000BK4', StrSubstNo(GraphUserHasExtraPlanTxt, TempPlan."Plan ID", Plan.Name), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', UserSetupCategoryTxt);
                    exit(true);
                end;
            until TempPlan.Next() = 0;
    end;

    [EventSubscriber(ObjectType::Table, Database::User, OnAfterDeleteEvent, '', true, true)]
    local procedure OnAfterDeleteUser(var Rec: Record User; RunTrigger: Boolean)
    var
        UserPlan: Record "User Plan";
    begin
        if Rec.IsTemporary() then
            exit;

        UserPlan.SetRange("User Security ID", Rec."User Security ID");
        UserPlan.DeleteAll();
    end;

    [InternalEvent(false)]
    local procedure OnBeforeIsBcServicePlan(var Skip: Boolean)
    begin
    end;

    [InternalEvent(false)]
    local procedure OnInitializeRoleCenter(var RoleCenterId: Integer; var Handled: Boolean)
    begin
    end;
}

Procedures, 26

NameParametersReturnsAccessObsolete
IsPlanAssigned(Guid)Booleanpublic-
IsPlanAssignedToUser(Guid)Booleanpublic-
IsPlanAssignedToUser(Guid, Guid)Booleanpublic-
IsGraphUserEntitledFromServicePlan(var DotNet UserInfo)Booleanpublic-
UpdateUserPlans(Guid, var DotNet UserInfo, Boolean, Boolean)public-
UpdateUserPlans(Guid, Boolean, Boolean, Boolean)public-
UpdateUserPlans()public-
RefreshUserPlanAssignments(Guid)public-
TryGetAzureUserPlanRoleCenterId(var Integer, Guid)Booleanpublic-
DoPlansExist()Booleanpublic-
DoUserPlansExist()Booleanpublic-
DoesPlanExist(Guid)Booleanpublic-
DoesUserHavePlans(Guid)Booleanpublic-
GetAvailablePlansCount()Integerpublic-
MixedPlansExist()Booleanpublic-
GetPlanIDs(DotNet UserInfo, var List)public-
GetPlanNames(DotNet UserInfo, var List)public-
GetPlanNames(Guid, var List)public-
CheckIfPlansDifferent(DotNet UserInfo, Guid)Booleanpublic-
IsBCServicePlan(Guid)Booleanpublic-
GetAllPlanIds()Listpublic-
AssignPlanToUserWithDelegatedRole(Guid)public-
GetUserPlanExperience()Enum User Plan Experiencepublic-
CheckMixedPlansExist()Booleanpublic-
CheckMixedPlansExist(Dictionary)Booleanpublic-
MixedPlansExist(Dictionary)Booleanpublic-

Events, 2

KindNameParametersObsolete
Internal eventOnBeforeIsBcServicePlan(var Boolean)-
Internal eventOnInitializeRoleCenter(var Integer, var Boolean)-