diff --git a/src/Apps/W1/ExpenseAgent/app/src/Common/ExpenseAgentAPIValidation.Codeunit.al b/src/Apps/W1/ExpenseAgent/app/src/Common/ExpenseAgentAPIValidation.Codeunit.al index ba65a32fc36..6f7f6ba9027 100644 --- a/src/Apps/W1/ExpenseAgent/app/src/Common/ExpenseAgentAPIValidation.Codeunit.al +++ b/src/Apps/W1/ExpenseAgent/app/src/Common/ExpenseAgentAPIValidation.Codeunit.al @@ -6,7 +6,6 @@ namespace Microsoft.ExpenseAgent; using System.AI; using System.Environment; -using System.Environment.Configuration; codeunit 6993 "Expense Agent API Validation" { @@ -17,7 +16,6 @@ codeunit 6993 "Expense Agent API Validation" var AgentNotEnabledErr: Label 'Expense Agent is not enabled. Please contact your administrator.'; CapabilityNotEnabledErr: Label 'The "%1" capability is not enabled. Please contact your administrator to enable the capability.', Comment = '%1 = a capability name, such as Expense Agent'; - ExpenseAgentAadAppIdTxt: Label 'ee1eb5fd-719b-44f2-97d0-0efd34bc4148', Locked = true; procedure VerifyAgentAccess() begin @@ -46,32 +44,24 @@ codeunit 6993 "Expense Agent API Validation" end; procedure GetAadAppId(): Text + var + ExpenseAgentEntraApp: Codeunit "Expense Agent Entra App Mgt."; begin - exit(ExpenseAgentAadAppIdTxt); + exit(ExpenseAgentEntraApp.GetAadAppId()); end; procedure IsCurrentUserExpenseAgent(): Boolean var - AADApplication: Record "AAD Application"; - EnvironmentInfo: Codeunit "Environment Information"; + ExpenseAgentEntraApp: Codeunit "Expense Agent Entra App Mgt."; begin - if not EnvironmentInfo.IsSaaSInfrastructure() then - exit(true); - - if not AADApplication.Get(ExpenseAgentAadAppIdTxt) then - exit(false); - - exit(AADApplication."User ID" = UserSecurityId()); + exit(ExpenseAgentEntraApp.IsCurrentUserExpenseAgent()); end; [TryFunction] internal procedure TryGetExpenseAgentUserId(var ExpenseAgentUserId: Guid) var - AadApplication: Record "AAD Application"; + ExpenseAgentEntraApp: Codeunit "Expense Agent Entra App Mgt."; begin - AadApplication.Get(ExpenseAgentAadAppIdTxt); - AadApplication.TestField(State, AadApplication.State::Enabled); - - ExpenseAgentUserId := AadApplication."User ID"; + ExpenseAgentUserId := ExpenseAgentEntraApp.GetEnabledExpenseAgentUserId(); end; } diff --git a/src/Apps/W1/ExpenseAgent/app/src/Integration/EAHttpClient.Codeunit.al b/src/Apps/W1/ExpenseAgent/app/src/Integration/EAHttpClient.Codeunit.al index d1a1c1334e1..fd611079291 100644 --- a/src/Apps/W1/ExpenseAgent/app/src/Integration/EAHttpClient.Codeunit.al +++ b/src/Apps/W1/ExpenseAgent/app/src/Integration/EAHttpClient.Codeunit.al @@ -589,13 +589,13 @@ codeunit 6941 "EA Http Client" var ExpenseAgentSetup: Record "Expense Agent Setup"; AzureADMgt: Codeunit "Azure AD Mgt."; - ExpenseAgentAPIValidation: Codeunit "Expense Agent API Validation"; + ExpenseAgentEntraApp: Codeunit "Expense Agent Entra App Mgt."; OAuth2: Codeunit OAuth2; Scopes: List of [Text]; OAuthScope: Text; OAuthScopePatternLbl: Label 'api://%1/', Locked = true; begin - OAuthScope := StrSubstNo(OAuthScopePatternLbl, ExpenseAgentAPIValidation.GetAadAppId()); + OAuthScope := StrSubstNo(OAuthScopePatternLbl, ExpenseAgentEntraApp.GetAadAppId()); Scopes.Add(OAuthScope + 'Expenses.ReadWrite.All'); AccessToken := AzureADMgt.GetAccessTokenAsSecretText(OAuthScope, '', false); if AccessToken.IsEmpty() then begin diff --git a/src/Apps/W1/ExpenseAgent/app/src/Setup/Codeunits/ExpenseAgentEntraAppMgt.Codeunit.al b/src/Apps/W1/ExpenseAgent/app/src/Setup/Codeunits/ExpenseAgentEntraAppMgt.Codeunit.al new file mode 100644 index 00000000000..1d64396fe2d --- /dev/null +++ b/src/Apps/W1/ExpenseAgent/app/src/Setup/Codeunits/ExpenseAgentEntraAppMgt.Codeunit.al @@ -0,0 +1,305 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.ExpenseAgent; + +using System.Agents; +using System.Environment; +using System.Environment.Configuration; +using System.Security.AccessControl; +using System.Security.User; + +codeunit 6913 "Expense Agent Entra App Mgt." +{ + Access = Internal; + InherentEntitlements = X; + InherentPermissions = X; + Permissions = + tabledata "AAD Application" = rm, + tabledata "Access Control" = rimd; + + internal procedure EnableAadApplicationForCurrentCompany() + begin + VerifyCurrentUserCanManageExpenseAgent(); + EnableAadApplicationForCurrentCompanyWithoutAuthorization(); + end; + + internal procedure DisableAadApplicationForCurrentCompany() + begin + VerifyCanDisableAadApplicationForCurrentCompany(); + DisableAadApplicationForCompany(GetCurrentCompanyName()); + end; + + internal procedure IsCurrentUserExpenseAgent(): Boolean + var + AadApplication: Record "AAD Application"; + EnvironmentInfo: Codeunit "Environment Information"; + begin + if not EnvironmentInfo.IsSaaSInfrastructure() then + exit(true); + + if not AadApplication.Get(GetAadAppId()) then + exit(false); + + exit(AadApplication."User ID" = UserSecurityId()); + end; + + internal procedure GetEnabledExpenseAgentUserId(): Guid + var + AadApplication: Record "AAD Application"; + begin + AadApplication.Get(GetAadAppId()); + AadApplication.TestField(State, AadApplication.State::Enabled); + + exit(AadApplication."User ID"); + end; + + internal procedure GetAadAppId(): Text + begin + exit(ExpenseAgentAadAppIdTxt); + end; + + internal procedure VerifyCanDisableAadApplicationForCurrentCompany() + begin + VerifyCurrentUserCanManageExpenseAgent(); + end; + + local procedure EnableAadApplicationForCurrentCompanyWithoutAuthorization() + var + AadApplication: Record "AAD Application"; + begin + GetAadApplication(AadApplication); + + // Enabling creates the application user. Disable it again before changing permissions. + if AadApplication.State <> AadApplication.State::Enabled then begin + AadApplication.Validate(State, AadApplication.State::Enabled); + AadApplication.Modify(true); + end; + + if HasPermissionForCurrentCompany(AadApplication) then + exit; + + AadApplication.Validate(State, AadApplication.State::Disabled); + AadApplication.Modify(true); + + AddPermissionForCompany(AadApplication, GetCurrentCompanyName()); + + AadApplication.Validate(State, AadApplication.State::Enabled); + AadApplication.Modify(true); + end; + + local procedure DisableAadApplicationForCompany(CompanyNameValue: Text[30]) + var + AadApplication: Record "AAD Application"; + OtherCompanyPermissionExists: Boolean; + begin + if not AadApplication.Get(GetAadAppId()) then + exit; + + OtherCompanyPermissionExists := HasOtherCompanyPermission(AadApplication, CompanyNameValue); + RemovePermissionForCompany(AadApplication, CompanyNameValue, not OtherCompanyPermissionExists); + if OtherCompanyPermissionExists then + exit; + if AadApplication.State = AadApplication.State::Disabled then + exit; + + AadApplication.Validate(State, AadApplication.State::Disabled); + AadApplication.Modify(true); + end; + + local procedure VerifyCurrentUserCanManageExpenseAgent() + var + AggregatePermissionSet: Record "Aggregate Permission Set"; + AgentSystemPermissions: Codeunit "Agent System Permissions"; + UserPermissions: Codeunit "User Permissions"; + begin + if UserPermissions.IsSuper(UserSecurityId()) then + exit; + + GetAgentAdminPermissionSet(AggregatePermissionSet); + if not UserPermissions.HasUserPermissionSetAssigned( + UserSecurityId(), + GetCurrentCompanyName(), + AggregatePermissionSet."Role ID", + AggregatePermissionSet.Scope, + AggregatePermissionSet."App ID") + then + Error(PermissionSetRequiredErr, AggregatePermissionSet.Name); + + if not AgentSystemPermissions.CurrentUserHasCanManageAllAgentsPermission() then + Error(PermissionSetRequiredErr, AggregatePermissionSet.Name); + + GetExpenseManagementAdminPermissionSet(AggregatePermissionSet); + if not UserPermissions.HasUserPermissionSetAssigned( + UserSecurityId(), + GetCurrentCompanyName(), + AggregatePermissionSet."Role ID", + AggregatePermissionSet.Scope, + AggregatePermissionSet."App ID") + then + Error(PermissionSetRequiredErr, AggregatePermissionSet.Name); + + if not HasSecurityPermission(UserPermissions) then + Error(SecurityPermissionRequiredErr); + + GetExpenseAgentPermissionSet(AggregatePermissionSet); + if not UserPermissions.HasUserPermissionSetAssigned( + UserSecurityId(), + GetCurrentCompanyName(), + AggregatePermissionSet."Role ID", + AggregatePermissionSet.Scope, + AggregatePermissionSet."App ID") + then + Error(PermissionSetRequiredErr, AggregatePermissionSet.Name); + end; + + local procedure HasSecurityPermission(UserPermissions: Codeunit "User Permissions"): Boolean + var + AccessControl: Record "Access Control"; + NullGuid: Guid; + begin + exit(UserPermissions.HasUserPermissionSetAssigned( + UserSecurityId(), + GetCurrentCompanyName(), + SecurityPermissionSetLbl, + AccessControl.Scope::System, + NullGuid)); + end; + + local procedure HasPermissionForCurrentCompany(AadApplication: Record "AAD Application"): Boolean + begin + exit(HasPermissionForCompany(AadApplication, GetCurrentCompanyName())); + end; + + local procedure HasPermissionForCompany(AadApplication: Record "AAD Application"; CompanyNameValue: Text[30]): Boolean + var + AccessControl: Record "Access Control"; + AggregatePermissionSet: Record "Aggregate Permission Set"; + begin + GetExpenseAgentPermissionSet(AggregatePermissionSet); + + SetExpenseAgentPermissionFilters(AccessControl, AadApplication, AggregatePermissionSet); + AccessControl.SetRange("Company Name", CompanyNameValue); + exit(not AccessControl.IsEmpty()); + end; + + local procedure HasOtherCompanyPermission(AadApplication: Record "AAD Application"; CompanyNameValue: Text[30]): Boolean + var + AccessControl: Record "Access Control"; + AggregatePermissionSet: Record "Aggregate Permission Set"; + begin + GetExpenseAgentPermissionSet(AggregatePermissionSet); + SetExpenseAgentPermissionFilters(AccessControl, AadApplication, AggregatePermissionSet); + AccessControl.SecurityFiltering(SecurityFilter::Ignored); + AccessControl.SetFilter("Company Name", '<>''''&<>%1', CompanyNameValue); + exit(not AccessControl.IsEmpty()); + end; + + local procedure AddPermissionForCompany(AadApplication: Record "AAD Application"; CompanyNameValue: Text[30]) + var + AccessControl: Record "Access Control"; + AggregatePermissionSet: Record "Aggregate Permission Set"; + begin + if HasPermissionForCompany(AadApplication, CompanyNameValue) then + exit; + + GetExpenseAgentPermissionSet(AggregatePermissionSet); + + AccessControl.Init(); + AccessControl.Validate("User Security ID", AadApplication."User ID"); + AccessControl.Validate("Role ID", AggregatePermissionSet."Role ID"); + AccessControl.Validate("App ID", AggregatePermissionSet."App ID"); + AccessControl.Validate(Scope, AggregatePermissionSet.Scope); + AccessControl.Validate("Company Name", CompanyNameValue); + AccessControl.Insert(true); + end; + + local procedure RemovePermissionForCompany(AadApplication: Record "AAD Application"; CompanyNameValue: Text[30]; RunTrigger: Boolean) + var + AccessControl: Record "Access Control"; + AggregatePermissionSet: Record "Aggregate Permission Set"; + begin + GetExpenseAgentPermissionSet(AggregatePermissionSet); + + SetExpenseAgentPermissionFilters(AccessControl, AadApplication, AggregatePermissionSet); + AccessControl.SetRange("Company Name", CompanyNameValue); + AccessControl.DeleteAll(RunTrigger); + end; + + local procedure GetAadApplication(var AadApplication: Record "AAD Application") + begin + if not AadApplication.Get(GetAadAppId()) then + Error(AadApplicationMissingErr); + end; + + local procedure GetCurrentCompanyName(): Text[30] + begin + exit(CopyStr(CompanyName(), 1, 30)); + end; + + local procedure GetExpenseAgentPermissionSet(var AggregatePermissionSet: Record "Aggregate Permission Set") + begin + GetPermissionSet(AggregatePermissionSet, ExpenseAgentPermissionSetLbl); + end; + + local procedure GetAgentAdminPermissionSet(var AggregatePermissionSet: Record "Aggregate Permission Set") + var + BaseApplicationAppId: Guid; + begin + Evaluate(BaseApplicationAppId, BaseApplicationAppIdTxt); + AggregatePermissionSet.Reset(); + AggregatePermissionSet.SetRange("App ID", BaseApplicationAppId); + AggregatePermissionSet.SetRange("Role ID", AgentAdminPermissionSetLbl); + if not AggregatePermissionSet.FindFirst() then + ErrorPermissionSetMissing(AgentAdminPermissionSetLbl); + end; + + local procedure GetExpenseManagementAdminPermissionSet(var AggregatePermissionSet: Record "Aggregate Permission Set") + begin + GetPermissionSet(AggregatePermissionSet, ExpenseManagementAdminPermissionSetLbl); + end; + + local procedure GetPermissionSet(var AggregatePermissionSet: Record "Aggregate Permission Set"; PermissionSetId: Code[20]) + var + ExpenseAgentAppId: Guid; + begin + AggregatePermissionSet.Reset(); + Evaluate(ExpenseAgentAppId, ExpenseAgentAppIdTxt); + AggregatePermissionSet.SetRange("App ID", ExpenseAgentAppId); + AggregatePermissionSet.SetRange("Role ID", PermissionSetId); + if not AggregatePermissionSet.FindFirst() then + ErrorPermissionSetMissing(PermissionSetId); + end; + + local procedure ErrorPermissionSetMissing(PermissionSetId: Code[20]) + var + MissingPermissionSetErrorInfo: ErrorInfo; + begin + MissingPermissionSetErrorInfo.ErrorType := ErrorType::Internal; + MissingPermissionSetErrorInfo.DataClassification := DataClassification::SystemMetadata; + MissingPermissionSetErrorInfo.Message := StrSubstNo(PermissionSetMissingErr, PermissionSetId); + Error(MissingPermissionSetErrorInfo); + end; + + local procedure SetExpenseAgentPermissionFilters(var AccessControl: Record "Access Control"; AadApplication: Record "AAD Application"; AggregatePermissionSet: Record "Aggregate Permission Set") + begin + AccessControl.SetRange("User Security ID", AadApplication."User ID"); + AccessControl.SetRange("Role ID", AggregatePermissionSet."Role ID"); + AccessControl.SetRange(Scope, AggregatePermissionSet.Scope); + AccessControl.SetRange("App ID", AggregatePermissionSet."App ID"); + end; + + var + ExpenseAgentAadAppIdTxt: Label 'ee1eb5fd-719b-44f2-97d0-0efd34bc4148', Locked = true; + ExpenseAgentAppIdTxt: Label '66efe10c-8033-403b-a86d-77c0887178ba', Locked = true; + BaseApplicationAppIdTxt: Label '437dbf0e-84ff-417a-965d-ed2bb9650972', Locked = true; + AgentAdminPermissionSetLbl: Label 'Agent - Admin', Locked = true; + ExpenseAgentPermissionSetLbl: Label 'Expense Agent', Locked = true; + ExpenseManagementAdminPermissionSetLbl: Label 'Expense Mgmt. Admin', Locked = true; + SecurityPermissionSetLbl: Label 'SECURITY', Locked = true; + AadApplicationMissingErr: Label 'The Expense Agent Microsoft Entra application is not configured.'; + PermissionSetRequiredErr: Label 'You must be assigned the %1 permission set to manage the Expense Agent Microsoft Entra application.', Comment = '%1 = permission set name'; + PermissionSetMissingErr: Label 'The %1 permission set is not available.', Comment = '%1 = permission set ID'; + SecurityPermissionRequiredErr: Label 'You must be assigned either the SUPER or SECURITY permission set to manage the Expense Agent Microsoft Entra application.'; +} diff --git a/src/Apps/W1/ExpenseAgent/app/src/Setup/Pages/ExpenseAgentSetupWizard.Page.al b/src/Apps/W1/ExpenseAgent/app/src/Setup/Pages/ExpenseAgentSetupWizard.Page.al index 0c67fe11c53..00fba63a99a 100644 --- a/src/Apps/W1/ExpenseAgent/app/src/Setup/Pages/ExpenseAgentSetupWizard.Page.al +++ b/src/Apps/W1/ExpenseAgent/app/src/Setup/Pages/ExpenseAgentSetupWizard.Page.al @@ -11,7 +11,6 @@ using System.AI; using System.Email; using System.Environment; using System.Environment.Configuration; -using System.Security.AccessControl; using System.Telemetry; using System.Utilities; #pragma warning disable AS0031 @@ -998,7 +997,6 @@ page 6991 "Expense Agent Setup Wizard" IncludeCategoriesForRulesQst: Label 'Default management rules require default expense categories. Do you want to add them to the configuration?'; IncludeCategoriesAndPostingGroupsForRulesQst: Label 'Default management rules require default expense categories and posting groups. Do you want to add them to the configuration?'; PrivacyNoticeNotAcceptedMsg: Label 'To use the Expense Agent, you must first accept the privacy notice. Please accept the privacy notice and try again.'; - ExpenseAgentPermissionSetLbl: Label 'Expense Agent', Locked = true; NoExpenseUsersErr: Label 'You must first specify who can access.'; NoSystemUsersErr: Label 'You must first specify a user in Business Central as expense user.'; NotAuthorizedToViewSetupErr: Label 'You do not have permission to view the Expense Agent setup. Contact your administrator to be granted agent management rights.'; @@ -1422,6 +1420,8 @@ page 6991 "Expense Agent Setup Wizard" end; local procedure ActivateAgent(): Boolean + var + ExpenseAgentEntraApp: Codeunit "Expense Agent Entra App Mgt."; begin ValidatePrivacyNoticeApproval(); ValidateCapabilityIsEnabled(); @@ -1430,7 +1430,7 @@ page 6991 "Expense Agent Setup Wizard" Error(ApprovalWorkflowConflictErr, Rec.FieldCaption("Enable Approval Workflow")); EnsureCurrentUserHasAccess(); - EnableAadApplication(); + ExpenseAgentEntraApp.EnableAadApplicationForCurrentCompany(); Commit(); if not RegisterErpConfiguration() then exit(false); @@ -1439,11 +1439,15 @@ page 6991 "Expense Agent Setup Wizard" end; local procedure DeactivateAgent(): Boolean + var + ExpenseAgentEntraApp: Codeunit "Expense Agent Entra App Mgt."; begin + ExpenseAgentEntraApp.VerifyCanDisableAadApplicationForCurrentCompany(); if not Rec.ShowDeactivationAccessWarning() then exit(false); if not UnregisterErpConfiguration() then exit(false); + ExpenseAgentEntraApp.DisableAadApplicationForCurrentCompany(); Rec.LogAgentDisabledTelemetry(); exit(true); end; @@ -1505,60 +1509,6 @@ page 6991 "Expense Agent Setup Wizard" Error(CapabilityDisabledErr, Enum::"Copilot Capability"::"Expense Agent"); end; - local procedure EnableAadApplication() - var - AadApplication: Record "AAD Application"; - ExpenseAgentApiValidation: Codeunit "Expense Agent API Validation"; - begin - AadApplication.SetRange("Client Id", ExpenseAgentApiValidation.GetAadAppId()); - if not AadApplication.FindFirst() then - exit; - - // We need to enable the AAD application first because enabling creates the user record. - // Once the user exists, we disable it, add the permission set, and re-enable it. - if AadApplication.State <> AadApplication.State::Enabled then begin - AadApplication.Validate(State, AadApplication.State::Enabled); - AadApplication.Modify(true); - end; - - if HasExpenseAgentPermissionSet(AadApplication) then - exit; - - AadApplication.Validate(State, AadApplication.State::Disabled); - AadApplication.Modify(true); - - AddExpenseAgentPermissionSet(AadApplication); - - AadApplication.Validate(State, AadApplication.State::Enabled); - AadApplication.Modify(true); - end; - - local procedure HasExpenseAgentPermissionSet(AadApplication: Record "AAD Application"): Boolean - var - AccessControl: Record "Access Control"; - begin - AccessControl.SetRange("User Security ID", AadApplication."User ID"); - AccessControl.SetRange("Role ID", ExpenseAgentPermissionSetLbl); - exit(not AccessControl.IsEmpty()); - end; - - local procedure AddExpenseAgentPermissionSet(AadApplication: Record "AAD Application") - var - AccessControl: Record "Access Control"; - AggregatePermissionSet: Record "Aggregate Permission Set"; - begin - AggregatePermissionSet.SetRange("Role ID", ExpenseAgentPermissionSetLbl); - if not AggregatePermissionSet.FindFirst() then - exit; - - AccessControl.Init(); - AccessControl.Validate("User Security ID", AadApplication."User ID"); - AccessControl.Validate("Role ID", ExpenseAgentPermissionSetLbl); - AccessControl.Validate("App ID", AggregatePermissionSet."App ID"); - AccessControl.Validate("Company Name", CopyStr(CompanyName(), 1, MaxStrLen(AccessControl."Company Name"))); - AccessControl.Insert(true); - end; - local procedure OnAssistEditMailbox() var PrevEmailAddress: Text[250]; diff --git a/src/Apps/W1/ExpenseAgent/test/app.json b/src/Apps/W1/ExpenseAgent/test/app.json index 0fca4afe54f..272bc182e1a 100644 --- a/src/Apps/W1/ExpenseAgent/test/app.json +++ b/src/Apps/W1/ExpenseAgent/test/app.json @@ -47,6 +47,10 @@ { "from": 148330, "to": 148350 + }, + { + "from": 148361, + "to": 148361 } ], "features": [ diff --git a/src/Apps/W1/ExpenseAgent/test/src/ExpenseAgentConfigTest.Codeunit.al b/src/Apps/W1/ExpenseAgent/test/src/ExpenseAgentConfigTest.Codeunit.al new file mode 100644 index 00000000000..dea0f478eae --- /dev/null +++ b/src/Apps/W1/ExpenseAgent/test/src/ExpenseAgentConfigTest.Codeunit.al @@ -0,0 +1,370 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.Test.ExpenseAgent; + +using Microsoft.ExpenseAgent; +using System.Environment.Configuration; +using System.Security.AccessControl; +using System.TestLibraries.Security.AccessControl; + +codeunit 148361 "Expense Agent Config. Test" +{ + Subtype = Test; + TestType = UnitTest; + TestPermissions = Disabled; + Permissions = + tabledata "AAD Application" = rm, + tabledata "Access Control" = rid; + + var + Assert: Codeunit Assert; + LibraryTestInitialize: Codeunit "Library - Test Initialize"; + UserPermissionsLibrary: Codeunit "User Permissions Library"; + ExpenseAgentAppIdTok: Label '66efe10c-8033-403b-a86d-77c0887178ba', Locked = true; + ExpenseAgentPermissionSetTok: Label 'Expense Agent', Locked = true; + UnrelatedPermissionSetTok: Label 'D365 BASIC', Locked = true; + + [Test] + procedure ActivatingWithOtherCompanyPermissionAddsCurrentCompanyPermission() + var + AadApplication: Record "AAD Application"; + ExpenseAgentEntraApp: Codeunit "Expense Agent Entra App Mgt."; + OtherCompanyName: Text[30]; + begin + // [SCENARIO 640454] Activating with another company permission adds the current company permission + Initialize(); + + // [GIVEN] Enabled Entra app "EA" has the Expense Agent permission only for company "B" + PrepareAadApplication(AadApplication, AadApplication.State::Enabled); + OtherCompanyName := GetOtherCompanyName(); + AssignPermission(AadApplication, ExpenseAgentPermissionSetTok, OtherCompanyName); + + // [WHEN] Activating the Expense Agent for the current company + ExpenseAgentEntraApp.EnableAadApplicationForCurrentCompany(); + + // [THEN] "EA" remains enabled and has Expense Agent permissions for both companies + VerifyAadApplicationState(AadApplication.State::Enabled); + VerifyPermissionExists(AadApplication, ExpenseAgentPermissionSetTok, GetCurrentCompanyName()); + VerifyPermissionExists(AadApplication, ExpenseAgentPermissionSetTok, OtherCompanyName); + end; + + [Test] + procedure ActivatingDisabledAppWithCurrentPermissionDoesNotDuplicatePermission() + var + AadApplication: Record "AAD Application"; + ExpenseAgentEntraApp: Codeunit "Expense Agent Entra App Mgt."; + begin + // [SCENARIO 640454] Activating a disabled Entra app preserves its existing current company permission + Initialize(); + + // [GIVEN] Disabled Entra app "EA" already has the Expense Agent permission for the current company + PrepareAadApplication(AadApplication, AadApplication.State::Disabled); + ExpenseAgentEntraApp.EnableAadApplicationForCurrentCompany(); + SetAadApplicationState(AadApplication.State::Disabled); + + // [WHEN] Activating the Expense Agent for the current company + ExpenseAgentEntraApp.EnableAadApplicationForCurrentCompany(); + + // [THEN] "EA" is enabled with one current company permission + VerifyAadApplicationState(AadApplication.State::Enabled); + VerifyPermissionCount(AadApplication, ExpenseAgentPermissionSetTok, GetCurrentCompanyName(), 1); + end; + + [Test] + procedure ActivatingWithUnrelatedPermissionPreservesPermission() + var + AadApplication: Record "AAD Application"; + ExpenseAgentEntraApp: Codeunit "Expense Agent Entra App Mgt."; + begin + // [SCENARIO 640454] Activating preserves an unrelated permission assigned to the Entra app user + Initialize(); + + // [GIVEN] Enabled Entra app "EA" has an unrelated permission but no Expense Agent permission + PrepareAadApplication(AadApplication, AadApplication.State::Enabled); + AssignPermission(AadApplication, UnrelatedPermissionSetTok, GetCurrentCompanyName()); + + // [WHEN] Activating the Expense Agent for the current company + ExpenseAgentEntraApp.EnableAadApplicationForCurrentCompany(); + + // [THEN] "EA" remains enabled with both the unrelated and current Expense Agent permissions + VerifyAadApplicationState(AadApplication.State::Enabled); + VerifyPermissionExists(AadApplication, UnrelatedPermissionSetTok, GetCurrentCompanyName()); + VerifyPermissionExists(AadApplication, ExpenseAgentPermissionSetTok, GetCurrentCompanyName()); + end; + + [Test] + procedure ActivatingWithGlobalPermissionAddsCurrentCompanyPermission() + var + AadApplication: Record "AAD Application"; + ExpenseAgentEntraApp: Codeunit "Expense Agent Entra App Mgt."; + begin + // [SCENARIO 640454] Activating with a global Expense Agent permission adds the current company permission + Initialize(); + + // [GIVEN] Enabled Entra app "EA" has a global Expense Agent permission + PrepareAadApplication(AadApplication, AadApplication.State::Enabled); + ExpenseAgentEntraApp.EnableAadApplicationForCurrentCompany(); + MakeCurrentExpenseAgentPermissionGlobal(AadApplication); + + // [WHEN] Activating the Expense Agent for the current company + ExpenseAgentEntraApp.EnableAadApplicationForCurrentCompany(); + + // [THEN] The global grant remains and an explicit current-company grant is added + VerifyAadApplicationState(AadApplication.State::Enabled); + VerifyPermissionExists(AadApplication, ExpenseAgentPermissionSetTok, ''); + VerifyPermissionExists(AadApplication, ExpenseAgentPermissionSetTok, GetCurrentCompanyName()); + end; + + [Test] + procedure DeactivatingPreservesOtherCompanyPermission() + var + AadApplication: Record "AAD Application"; + ExpenseAgentEntraApp: Codeunit "Expense Agent Entra App Mgt."; + OtherCompanyName: Text[30]; + begin + // [SCENARIO 640454] Deactivating preserves another company permission + Initialize(); + + // [GIVEN] Enabled Entra app "EA" has Expense Agent permissions for the current company and company "B" + PrepareAadApplication(AadApplication, AadApplication.State::Enabled); + OtherCompanyName := GetOtherCompanyName(); + ExpenseAgentEntraApp.EnableAadApplicationForCurrentCompany(); + AssignPermission(AadApplication, ExpenseAgentPermissionSetTok, OtherCompanyName); + VerifyPermissionExists(AadApplication, ExpenseAgentPermissionSetTok, OtherCompanyName); + + // [WHEN] Deactivating the Expense Agent for the current company + ExpenseAgentEntraApp.DisableAadApplicationForCurrentCompany(); + + // [THEN] The current permission is removed and company "B" is preserved + VerifyPermissionDoesNotExist(AadApplication, ExpenseAgentPermissionSetTok, GetCurrentCompanyName()); + VerifyPermissionExists(AadApplication, ExpenseAgentPermissionSetTok, OtherCompanyName); + end; + + [Test] + procedure DeactivatingRemovesCurrentCompanyPermission() + var + AadApplication: Record "AAD Application"; + ExpenseAgentEntraApp: Codeunit "Expense Agent Entra App Mgt."; + begin + // [SCENARIO 640454] Deactivating removes the current-company Expense Agent permission + Initialize(); + + // [GIVEN] Enabled Entra app "EA" has the Expense Agent permission only for the current company + PrepareAadApplication(AadApplication, AadApplication.State::Enabled); + ExpenseAgentEntraApp.EnableAadApplicationForCurrentCompany(); + + // [WHEN] Deactivating the Expense Agent for the current company + ExpenseAgentEntraApp.DisableAadApplicationForCurrentCompany(); + + // [THEN] The current-company Expense Agent permission is removed and "EA" is disabled + VerifyAadApplicationState(AadApplication.State::Disabled); + VerifyPermissionDoesNotExist(AadApplication, ExpenseAgentPermissionSetTok, GetCurrentCompanyName()); + end; + + [Test] + procedure DeactivatingPreservesUnrelatedPermission() + var + AadApplication: Record "AAD Application"; + ExpenseAgentEntraApp: Codeunit "Expense Agent Entra App Mgt."; + begin + // [SCENARIO 640454] Deactivating preserves unrelated permissions + Initialize(); + + // [GIVEN] Enabled Entra app "EA" has current Expense Agent and unrelated permissions + PrepareAadApplication(AadApplication, AadApplication.State::Enabled); + AssignPermission(AadApplication, UnrelatedPermissionSetTok, GetCurrentCompanyName()); + ExpenseAgentEntraApp.EnableAadApplicationForCurrentCompany(); + + // [WHEN] Deactivating the Expense Agent for the current company + ExpenseAgentEntraApp.DisableAadApplicationForCurrentCompany(); + + // [THEN] The Expense Agent permission is removed and the unrelated permission remains + VerifyPermissionDoesNotExist(AadApplication, ExpenseAgentPermissionSetTok, GetCurrentCompanyName()); + VerifyPermissionExists(AadApplication, UnrelatedPermissionSetTok, GetCurrentCompanyName()); + end; + + [Test] + procedure DeactivatingWithoutExpensePermissionPreservesUnrelatedPermission() + var + AadApplication: Record "AAD Application"; + ExpenseAgentEntraApp: Codeunit "Expense Agent Entra App Mgt."; + begin + // [SCENARIO 640454] Deactivating without a current grant preserves unrelated permissions + Initialize(); + + // [GIVEN] Enabled Entra app "EA" has only an unrelated permission + PrepareAadApplication(AadApplication, AadApplication.State::Enabled); + AssignPermission(AadApplication, UnrelatedPermissionSetTok, GetCurrentCompanyName()); + + // [WHEN] Deactivating the Expense Agent for the current company + ExpenseAgentEntraApp.DisableAadApplicationForCurrentCompany(); + + // [THEN] The unrelated permission remains + VerifyPermissionDoesNotExist(AadApplication, ExpenseAgentPermissionSetTok, GetCurrentCompanyName()); + VerifyPermissionExists(AadApplication, UnrelatedPermissionSetTok, GetCurrentCompanyName()); + end; + + [Test] + procedure DeactivatingWithGlobalPermissionDisablesApplication() + var + AadApplication: Record "AAD Application"; + ExpenseAgentEntraApp: Codeunit "Expense Agent Entra App Mgt."; + begin + // [SCENARIO 640454] A global grant does not keep the Entra app enabled after the last company is deactivated + Initialize(); + + // [GIVEN] Enabled Entra app "EA" has global and current-company Expense Agent permissions + PrepareAadApplication(AadApplication, AadApplication.State::Enabled); + ExpenseAgentEntraApp.EnableAadApplicationForCurrentCompany(); + MakeCurrentExpenseAgentPermissionGlobal(AadApplication); + ExpenseAgentEntraApp.EnableAadApplicationForCurrentCompany(); + + // [WHEN] Deactivating the Expense Agent for the current company + ExpenseAgentEntraApp.DisableAadApplicationForCurrentCompany(); + + // [THEN] "EA" is disabled, the current grant is removed, and the global grant remains + VerifyAadApplicationState(AadApplication.State::Disabled); + VerifyPermissionExists(AadApplication, ExpenseAgentPermissionSetTok, ''); + VerifyPermissionDoesNotExist(AadApplication, ExpenseAgentPermissionSetTok, GetCurrentCompanyName()); + end; + + local procedure Initialize() + begin + LibraryTestInitialize.OnTestInitialize(Codeunit::"Expense Agent Config. Test"); + end; + + local procedure PrepareAadApplication(var AadApplication: Record "AAD Application"; State: Option) + var + AccessControl: Record "Access Control"; + ExpenseAgentEntraApp: Codeunit "Expense Agent Entra App Mgt."; + begin + AadApplication.Get(ExpenseAgentEntraApp.GetAadAppId()); + if AadApplication.State <> State then begin + AadApplication.Validate(State, State); + AadApplication.Modify(true); + end; + + AccessControl.SetRange("User Security ID", AadApplication."User ID"); + AccessControl.DeleteAll(true); + end; + + local procedure AssignPermission(AadApplication: Record "AAD Application"; PermissionSetId: Code[20]; CompanyNameValue: Text[30]) + var + AggregatePermissionSet: Record "Aggregate Permission Set"; + begin + if PermissionSetId = ExpenseAgentPermissionSetTok then begin + GetExpenseAgentPermissionSet(AggregatePermissionSet); + AssignPermission(AadApplication, AggregatePermissionSet, CompanyNameValue); + exit; + end; + + UserPermissionsLibrary.AssignPermissionSetToUser(AadApplication."User ID", PermissionSetId, CompanyNameValue); + end; + + local procedure AssignPermission(AadApplication: Record "AAD Application"; AggregatePermissionSet: Record "Aggregate Permission Set"; CompanyNameValue: Text[30]) + var + AccessControl: Record "Access Control"; + begin + AadApplication.Get(AadApplication."Client Id"); + AccessControl.Init(); + AccessControl."User Security ID" := AadApplication."User ID"; + AccessControl."Role ID" := AggregatePermissionSet."Role ID"; + AccessControl."Company Name" := CompanyNameValue; + AccessControl.Scope := AggregatePermissionSet.Scope; + AccessControl."App ID" := AggregatePermissionSet."App ID"; + AccessControl.Insert(true); + end; + + local procedure GetOtherCompanyName(): Text[30] + begin + exit(CopyStr(Format(CreateGuid()), 1, 30)); + end; + + local procedure GetCurrentCompanyName(): Text[30] + begin + exit(CopyStr(CompanyName(), 1, 30)); + end; + + local procedure VerifyAadApplicationState(ExpectedState: Option) + var + AadApplication: Record "AAD Application"; + ExpenseAgentEntraApp: Codeunit "Expense Agent Entra App Mgt."; + begin + AadApplication.Get(ExpenseAgentEntraApp.GetAadAppId()); + Assert.AreEqual(ExpectedState, AadApplication.State, 'The Entra application state is incorrect.'); + end; + + local procedure SetAadApplicationState(State: Option) + var + AadApplication: Record "AAD Application"; + ExpenseAgentEntraApp: Codeunit "Expense Agent Entra App Mgt."; + begin + AadApplication.Get(ExpenseAgentEntraApp.GetAadAppId()); + AadApplication.Validate(State, State); + AadApplication.Modify(true); + end; + + local procedure VerifyPermissionExists(AadApplication: Record "AAD Application"; PermissionSetId: Code[20]; CompanyNameValue: Text[30]) + begin + VerifyPermissionCount(AadApplication, PermissionSetId, CompanyNameValue, 1); + end; + + local procedure VerifyPermissionDoesNotExist(AadApplication: Record "AAD Application"; PermissionSetId: Code[20]; CompanyNameValue: Text[30]) + begin + VerifyPermissionCount(AadApplication, PermissionSetId, CompanyNameValue, 0); + end; + + local procedure VerifyPermissionCount(AadApplication: Record "AAD Application"; PermissionSetId: Code[20]; CompanyNameValue: Text[30]; ExpectedCount: Integer) + var + AccessControl: Record "Access Control"; + AggregatePermissionSet: Record "Aggregate Permission Set"; + begin + AadApplication.Get(AadApplication."Client Id"); + AccessControl.SetRange("User Security ID", AadApplication."User ID"); + if PermissionSetId = ExpenseAgentPermissionSetTok then begin + GetExpenseAgentPermissionSet(AggregatePermissionSet); + AccessControl.SetRange(Scope, AggregatePermissionSet.Scope); + AccessControl.SetRange("App ID", AggregatePermissionSet."App ID"); + end; + AccessControl.SetRange("Role ID", PermissionSetId); + AccessControl.SetRange("Company Name", CompanyNameValue); + Assert.AreEqual(ExpectedCount, AccessControl.Count(), 'The number of matching Entra app permissions is incorrect.'); + end; + + local procedure GetExpenseAgentPermissionSet(var AggregatePermissionSet: Record "Aggregate Permission Set") + begin + GetPermissionSet(AggregatePermissionSet, ExpenseAgentPermissionSetTok); + end; + + local procedure GetPermissionSet(var AggregatePermissionSet: Record "Aggregate Permission Set"; PermissionSetId: Code[20]) + var + ExpenseAgentAppId: Guid; + begin + Evaluate(ExpenseAgentAppId, ExpenseAgentAppIdTok); + AggregatePermissionSet.SetRange("App ID", ExpenseAgentAppId); + AggregatePermissionSet.SetRange("Role ID", PermissionSetId); + AggregatePermissionSet.FindFirst(); + end; + + local procedure MakeCurrentExpenseAgentPermissionGlobal(AadApplication: Record "AAD Application") + var + AccessControl: Record "Access Control"; + AggregatePermissionSet: Record "Aggregate Permission Set"; + begin + GetExpenseAgentPermissionSet(AggregatePermissionSet); + AccessControl.Get( + AadApplication."User ID", + AggregatePermissionSet."Role ID", + GetCurrentCompanyName(), + AggregatePermissionSet.Scope, + AggregatePermissionSet."App ID"); + AccessControl.Rename( + AadApplication."User ID", + AggregatePermissionSet."Role ID", + '', + AggregatePermissionSet.Scope, + AggregatePermissionSet."App ID"); + end; +} diff --git a/src/Apps/W1/ExpenseAgent/test/src/ExpensePermissionsTest.Codeunit.al b/src/Apps/W1/ExpenseAgent/test/src/ExpensePermissionsTest.Codeunit.al index 9c4888740da..aae9d12e946 100644 --- a/src/Apps/W1/ExpenseAgent/test/src/ExpensePermissionsTest.Codeunit.al +++ b/src/Apps/W1/ExpenseAgent/test/src/ExpensePermissionsTest.Codeunit.al @@ -8,6 +8,9 @@ using Microsoft.ExpenseAgent; using Microsoft.Finance.SpendRequest; using Microsoft.HumanResources.Employee; using Microsoft.HumanResources.Setup; +using System.Environment.Configuration; +using System.Security.AccessControl; +using System.Security.User; codeunit 148338 "Expense Permissions Test" { @@ -22,11 +25,16 @@ codeunit 148338 "Expense Permissions Test" LibraryRandom: Codeunit "Library - Random"; LibraryTestInitialize: Codeunit "Library - Test Initialize"; IsInitialized: Boolean; + AgentAdminPermissionSetTok: Label 'Agent - Admin', Locked = true; + BaseApplicationAppIdTok: Label '437dbf0e-84ff-417a-965d-ed2bb9650972', Locked = true; EmployeeOnlyPermissionSetTok: Label 'Exp. Emp. Only Test', Locked = true; HREditPermissionSetTok: Label 'Exp. HR Edit Test', Locked = true; AutomationPermissionSetTok: Label 'Exp. Auto Test', Locked = true; D365BasicPermissionSetTok: Label 'D365 BASIC', Locked = true; ExpenseAgentPermissionSetTok: Label 'Expense Agent', Locked = true; + ExpenseAgentAppIdTok: Label '66efe10c-8033-403b-a86d-77c0887178ba', Locked = true; + ExpenseMgmtAdminPermissionSetTok: Label 'Expense Mgmt. Admin', Locked = true; + SecurityPermissionSetTok: Label 'SECURITY', Locked = true; PermissionDeniedErr: Label 'You do not have the following permissions', Locked = true; CannotDeleteEmployeeWithExpenseErr: Label 'You cannot delete Employee %1 because they have active expense.', Comment = '%1 = Employee No.'; CannotDeleteEmployeeWithExpenseReportErr: Label 'You cannot delete Employee %1 because they have active expense report.', Comment = '%1 = Employee No.'; @@ -300,6 +308,199 @@ codeunit 148338 "Expense Permissions Test" RestoreFullPermissions(); end; + [Test] + procedure SuperCanActivateWithoutAdditionalPermissionSets() + var + AadApplication: Record "AAD Application"; + ExpenseAgentEntraApp: Codeunit "Expense Agent Entra App Mgt."; + begin + // [SCENARIO 640454] SUPER can activate without additional Expense Agent administrator permission sets + Initialize(); + + // [GIVEN] Disabled Entra app "EA" without an Expense Agent permission + PrepareAadApplication(AadApplication, AadApplication.State::Disabled); + + // [GIVEN] SUPER user "U" has none of the additional Expense Agent administrator permission sets + PrepareCurrentUserPermissionAssignments(); + VerifySuperWithoutAdditionalExpenseAgentPermissionSets(); + + // [WHEN] "U" activates "EA" + ExpenseAgentEntraApp.EnableAadApplicationForCurrentCompany(); + + // [THEN] "EA" is enabled with one current-company Expense Agent permission + VerifyAadApplicationState(AadApplication.State::Enabled); + VerifyExpenseAgentPermissionCount(AadApplication, GetCurrentCompanyName(), 1); + end; + + [Test] + procedure SuperCanDeactivateWithoutAdditionalPermissionSets() + var + AadApplication: Record "AAD Application"; + ExpenseAgentEntraApp: Codeunit "Expense Agent Entra App Mgt."; + begin + // [SCENARIO 640454] SUPER can deactivate without additional Expense Agent administrator permission sets + Initialize(); + + // [GIVEN] Enabled Entra app "EA" with the current-company Expense Agent permission + PrepareAadApplication(AadApplication, AadApplication.State::Enabled); + AssignExpenseAgentPermission(AadApplication, GetCurrentCompanyName()); + + // [GIVEN] SUPER user "U" has none of the additional Expense Agent administrator permission sets + PrepareCurrentUserPermissionAssignments(); + VerifySuperWithoutAdditionalExpenseAgentPermissionSets(); + + // [WHEN] "U" deactivates "EA" + ExpenseAgentEntraApp.DisableAadApplicationForCurrentCompany(); + + // [THEN] The current-company Expense Agent permission is removed + VerifyExpenseAgentPermissionCount(AadApplication, GetCurrentCompanyName(), 0); + end; + + local procedure Initialize() + begin + LibraryTestInitialize.OnTestInitialize(Codeunit::"Expense Permissions Test"); + RestoreFullPermissions(); + LibraryExpense.CleanTransactionalData(); + LibraryExpense.CleanUpBeforeTesting(); + if IsInitialized then + exit; + + LibraryTestInitialize.OnBeforeTestSuiteInitialize(Codeunit::"Expense Permissions Test"); + EnsureSetupRecordsExist(); + LibraryExpense.SetupNumberSeriesInExpenseMgmt(); + LibraryExpense.UpdateEnableApprovalWorkflowInAgentSetup(false); + IsInitialized := true; + Commit(); + LibraryTestInitialize.OnAfterTestSuiteInitialize(Codeunit::"Expense Permissions Test"); + end; + + local procedure PrepareCurrentUserPermissionAssignments() + begin + RemoveCurrentUserPermissionSet(AgentAdminPermissionSetTok); + RemoveCurrentUserPermissionSet(ExpenseMgmtAdminPermissionSetTok); + RemoveCurrentUserPermissionSet(SecurityPermissionSetTok); + RemoveCurrentUserPermissionSet(ExpenseAgentPermissionSetTok); + end; + + local procedure VerifySuperWithoutAdditionalExpenseAgentPermissionSets() + var + AccessControl: Record "Access Control"; + AggregatePermissionSet: Record "Aggregate Permission Set"; + UserPermissions: Codeunit "User Permissions"; + BaseApplicationAppId: Guid; + NullGuid: Guid; + begin + Assert.IsTrue(UserPermissions.IsSuper(UserSecurityId()), 'The test user must be SUPER.'); + + Evaluate(BaseApplicationAppId, BaseApplicationAppIdTok); + AggregatePermissionSet.SetRange("App ID", BaseApplicationAppId); + AggregatePermissionSet.SetRange("Role ID", AgentAdminPermissionSetTok); + AggregatePermissionSet.FindFirst(); + Assert.IsFalse( + UserPermissions.HasUserPermissionSetAssigned( + UserSecurityId(), GetCurrentCompanyName(), AggregatePermissionSet."Role ID", AggregatePermissionSet.Scope, AggregatePermissionSet."App ID"), + 'Agent - Admin must not be assigned.'); + + GetExpensePermissionSet(AggregatePermissionSet, ExpenseMgmtAdminPermissionSetTok); + Assert.IsFalse( + UserPermissions.HasUserPermissionSetAssigned( + UserSecurityId(), GetCurrentCompanyName(), AggregatePermissionSet."Role ID", AggregatePermissionSet.Scope, AggregatePermissionSet."App ID"), + 'Expense Mgmt. Admin must not be assigned.'); + + Assert.IsFalse( + UserPermissions.HasUserPermissionSetAssigned( + UserSecurityId(), GetCurrentCompanyName(), SecurityPermissionSetTok, AccessControl.Scope::System, NullGuid), + 'SECURITY must not be assigned.'); + + GetExpenseAgentPermissionSet(AggregatePermissionSet); + Assert.IsFalse( + UserPermissions.HasUserPermissionSetAssigned( + UserSecurityId(), GetCurrentCompanyName(), AggregatePermissionSet."Role ID", AggregatePermissionSet.Scope, AggregatePermissionSet."App ID"), + 'Expense Agent must not be assigned.'); + end; + + local procedure RemoveCurrentUserPermissionSet(PermissionSetId: Code[20]) + var + AccessControl: Record "Access Control"; + begin + AccessControl.SetRange("User Security ID", UserSecurityId()); + AccessControl.SetRange("Role ID", PermissionSetId); + AccessControl.DeleteAll(true); + end; + + local procedure PrepareAadApplication(var AadApplication: Record "AAD Application"; State: Option) + var + AccessControl: Record "Access Control"; + ExpenseAgentEntraApp: Codeunit "Expense Agent Entra App Mgt."; + begin + AadApplication.Get(ExpenseAgentEntraApp.GetAadAppId()); + if AadApplication.State <> State then begin + AadApplication.Validate(State, State); + AadApplication.Modify(true); + end; + + AccessControl.SetRange("User Security ID", AadApplication."User ID"); + AccessControl.DeleteAll(true); + end; + + local procedure AssignExpenseAgentPermission(AadApplication: Record "AAD Application"; CompanyNameValue: Text[30]) + var + AccessControl: Record "Access Control"; + AggregatePermissionSet: Record "Aggregate Permission Set"; + begin + GetExpenseAgentPermissionSet(AggregatePermissionSet); + AccessControl.Init(); + AccessControl."User Security ID" := AadApplication."User ID"; + AccessControl."Role ID" := AggregatePermissionSet."Role ID"; + AccessControl."Company Name" := CompanyNameValue; + AccessControl.Scope := AggregatePermissionSet.Scope; + AccessControl."App ID" := AggregatePermissionSet."App ID"; + AccessControl.Insert(true); + end; + + local procedure GetExpenseAgentPermissionSet(var AggregatePermissionSet: Record "Aggregate Permission Set") + begin + GetExpensePermissionSet(AggregatePermissionSet, ExpenseAgentPermissionSetTok); + end; + + local procedure GetExpensePermissionSet(var AggregatePermissionSet: Record "Aggregate Permission Set"; PermissionSetId: Code[20]) + var + ExpenseAgentAppId: Guid; + begin + Evaluate(ExpenseAgentAppId, ExpenseAgentAppIdTok); + AggregatePermissionSet.SetRange("App ID", ExpenseAgentAppId); + AggregatePermissionSet.SetRange("Role ID", PermissionSetId); + AggregatePermissionSet.FindFirst(); + end; + + local procedure GetCurrentCompanyName(): Text[30] + begin + exit(CopyStr(CompanyName(), 1, 30)); + end; + + local procedure VerifyAadApplicationState(ExpectedState: Option) + var + AadApplication: Record "AAD Application"; + ExpenseAgentEntraApp: Codeunit "Expense Agent Entra App Mgt."; + begin + AadApplication.Get(ExpenseAgentEntraApp.GetAadAppId()); + Assert.AreEqual(ExpectedState, AadApplication.State, 'The Entra application state is incorrect.'); + end; + + local procedure VerifyExpenseAgentPermissionCount(AadApplication: Record "AAD Application"; CompanyNameValue: Text[30]; ExpectedCount: Integer) + var + AccessControl: Record "Access Control"; + AggregatePermissionSet: Record "Aggregate Permission Set"; + begin + GetExpenseAgentPermissionSet(AggregatePermissionSet); + AccessControl.SetRange("User Security ID", AadApplication."User ID"); + AccessControl.SetRange("Role ID", AggregatePermissionSet."Role ID"); + AccessControl.SetRange("Company Name", CompanyNameValue); + AccessControl.SetRange(Scope, AggregatePermissionSet.Scope); + AccessControl.SetRange("App ID", AggregatePermissionSet."App ID"); + Assert.AreEqual(ExpectedCount, AccessControl.Count(), 'The number of matching Expense Agent permissions is incorrect.'); + end; + local procedure VerifyExpenseMgmtPermissions(PermissionSetId: Code[20]; CanEdit: Boolean) var SpendRequest: Record "Spend Request"; @@ -431,24 +632,6 @@ codeunit 148338 "Expense Permissions Test" PostedExpenseReportHeader.Insert(false); end; - local procedure Initialize() - begin - LibraryTestInitialize.OnTestInitialize(Codeunit::"Expense Permissions Test"); - RestoreFullPermissions(); - LibraryExpense.CleanTransactionalData(); - LibraryExpense.CleanUpBeforeTesting(); - if IsInitialized then - exit; - - LibraryTestInitialize.OnBeforeTestSuiteInitialize(Codeunit::"Expense Permissions Test"); - EnsureSetupRecordsExist(); - LibraryExpense.SetupNumberSeriesInExpenseMgmt(); - LibraryExpense.UpdateEnableApprovalWorkflowInAgentSetup(false); - IsInitialized := true; - Commit(); - LibraryTestInitialize.OnAfterTestSuiteInitialize(Codeunit::"Expense Permissions Test"); - end; - local procedure VerifyPermissionSetCanInsertActivity(PermissionSetId: Code[20]) var ExpenseUser: Record "Expense User";