diff --git a/src/System Application/App/Extension Management/app.json b/src/System Application/App/Extension Management/app.json index 6344cdf81ee..0333a369adb 100644 --- a/src/System Application/App/Extension Management/app.json +++ b/src/System Application/App/Extension Management/app.json @@ -76,6 +76,12 @@ "name": "VS Code Integration", "publisher": "Microsoft", "version": "25.3.0.0" + }, + { + "id": "c56e3ef4-7ab0-4636-ae87-013a62f12213", + "name": "User Permissions", + "publisher": "Microsoft", + "version": "25.3.0.0" } ], "internalsVisibleTo": [ diff --git a/src/System Application/App/Extension Management/permissions/ExtenMgtAdmin.PermissionSet.al b/src/System Application/App/Extension Management/permissions/ExtenMgtAdmin.PermissionSet.al index 720da8efa42..620828d9b05 100644 --- a/src/System Application/App/Extension Management/permissions/ExtenMgtAdmin.PermissionSet.al +++ b/src/System Application/App/Extension Management/permissions/ExtenMgtAdmin.PermissionSet.al @@ -17,7 +17,7 @@ permissionset 2502 "Exten. Mgt. - Admin" "Guided Experience - View"; Permissions = tabledata "Application Dependency" = Rimd, - tabledata "Application Object Metadata" = Rimd, // r needed for check CanManageExtensions + tabledata "Application Object Metadata" = Rimd, tabledata "Application Resource" = Rimd, tabledata "Extension Pending Setup" = rimd, tabledata "Inplace Installed Application" = Rimd, diff --git a/src/System Application/App/Extension Management/src/ExtensionInstallationImpl.Codeunit.al b/src/System Application/App/Extension Management/src/ExtensionInstallationImpl.Codeunit.al index 3c7f340cc56..69cfa0dbe74 100644 --- a/src/System Application/App/Extension Management/src/ExtensionInstallationImpl.Codeunit.al +++ b/src/System Application/App/Extension Management/src/ExtensionInstallationImpl.Codeunit.al @@ -5,8 +5,10 @@ namespace System.Apps; -using System.Utilities; using System.Environment.Configuration; +using System.Security.AccessControl; +using System.Security.User; +using System.Utilities; codeunit 2500 "Extension Installation Impl" { @@ -181,11 +183,55 @@ codeunit 2500 "Extension Installation Impl" Error(NotSufficientPermissionErr); end; + procedure CheckPermissions(UserSecurityIdToCheck: Guid) + begin + if not CanManageExtensions(UserSecurityIdToCheck) then + Error(NotSufficientPermissionErr); + end; + procedure CanManageExtensions(): Boolean var - ApplicationObjectMetadata: Record "Application Object Metadata"; + UserPermissions: Codeunit "User Permissions"; + Result: Boolean; + begin + OnCanManageExtensions(Result); + if Result then + exit(true); + + if UserPermissions.IsSuper(UserSecurityId()) then + exit(true); + + exit(CanManageExtensions(UserSecurityId())); + end; + + procedure CanManageExtensions(UserSecurityIdToCheck: Guid): Boolean + var + AccessControl: Record "Access Control"; + UserPermissions: Codeunit "User Permissions"; + CurrentModuleInfo: ModuleInfo; + ExtensionManagementAdminTok: Label 'Exten. Mgt. - Admin', Locked = true; + SuperTok: Label 'SUPER', Locked = true; + NullGuid: Guid; + begin + if UserPermissions.HasUserPermissionSetAssigned( + UserSecurityIdToCheck, '', SuperTok, AccessControl.Scope::System, NullGuid) + then + exit(true); + + NavApp.GetCurrentModuleInfo(CurrentModuleInfo); + exit(UserPermissions.HasUserPermissionSetAssigned( + UserSecurityIdToCheck, '', ExtensionManagementAdminTok, AccessControl.Scope::System, CurrentModuleInfo.Id())); + end; + + /// + /// Allows tests to authorize the current test session without modifying platform user records. + /// + /// + /// This event is for testing only and must not be used in production. + /// + [InternalEvent(false)] + local procedure OnCanManageExtensions(var Result: Boolean) begin - exit(ApplicationObjectMetadata.ReadPermission()); end; procedure UninstallExtension(PackageID: Guid; IsUIEnabled: Boolean): Boolean @@ -418,4 +464,3 @@ codeunit 2500 "Extension Installation Impl" exit(Result = PolicyToTestFor); end; } - diff --git a/src/System Application/Test/Extension Management/permissions/ExtMgtNonAdmin.PermissionSet.al b/src/System Application/Test/Extension Management/permissions/ExtMgtNonAdmin.PermissionSet.al new file mode 100644 index 00000000000..e9856758419 --- /dev/null +++ b/src/System Application/Test/Extension Management/permissions/ExtMgtNonAdmin.PermissionSet.al @@ -0,0 +1,15 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.Test.Apps; + +using System.Security.AccessControl; + +permissionset 133100 "Ext. Mgt. Nonadmin" +{ + Assignable = true; + + Permissions = tabledata "Access Control" = R; +} \ No newline at end of file diff --git a/src/System Application/Test/Extension Management/src/ExtensionManagementTest.Codeunit.al b/src/System Application/Test/Extension Management/src/ExtensionManagementTest.Codeunit.al index 636abc61c4c..2ab86fef953 100644 --- a/src/System Application/Test/Extension Management/src/ExtensionManagementTest.Codeunit.al +++ b/src/System Application/Test/Extension Management/src/ExtensionManagementTest.Codeunit.al @@ -5,14 +5,16 @@ namespace System.Test.Apps; -using System.Environment.Configuration; using System.Apps; +using System.Environment.Configuration; using System.Media; -using System.TestLibraries.Utilities; +using System.Security.AccessControl; using System.TestLibraries.Security.AccessControl; +using System.TestLibraries.Utilities; codeunit 133100 "Extension Management Test" { + EventSubscriberInstance = Manual; // For the purpose of this test, two sample extensions are pre-published. // The condition for the tests to pass is that one of the extensions is holding a dependency on the other one, and they are both 1st Party Extensions, version 1.0. @@ -28,8 +30,11 @@ codeunit 133100 "Extension Management Test" var ExtensionManagement: Codeunit "Extension Management"; + ExtensionManagementTest: Codeunit "Extension Management Test"; + ExtensionInstallationImplTest: Codeunit "Extension Installation Impl"; Assert: Codeunit "Library Assert"; PermissionsMock: Codeunit "Permissions Mock"; + ExtensionManagementTestBound: Boolean; MainAppId: Guid; DependingAppId: Guid; NotInstalledSuccErr: Label 'Extension was not installed succesfully'; @@ -40,6 +45,7 @@ codeunit 133100 "Extension Management Test" ExtensionNotInstalledErr: Label 'Extension should be installed.'; PackageIdExistsErr: Label 'The returned extension pakage does not exist'; NullPackageIdErr: Label 'There should not be an extension corresponding to the returned package ID'; + NotSufficientPermissionErr: Label 'You do not have sufficient permissions to manage extensions. Please contact your administrator.'; PackageIdExtensionVersionErr: Label 'The package Id does not poin to the correct extension version'; local procedure SetNavAppIds() @@ -48,16 +54,122 @@ codeunit 133100 "Extension Management Test" DependingAppId := 'c4123d81-a537-4062-bdd4-7b9882bcc319'; end; + local procedure CreateUserWithPermissionSet(RoleId: Code[20]; Company: Text): Guid + var + AccessControl: Record "Access Control"; + AggregatePermissionSet: Record "Aggregate Permission Set"; + UserSecurityId: Guid; + begin + UserSecurityId := CreateGuid(); + + AggregatePermissionSet.SetRange("Role ID", RoleId); + AggregatePermissionSet.FindFirst(); + + AccessControl.Init(); + AccessControl."User Security ID" := UserSecurityId; + AccessControl."Role ID" := AggregatePermissionSet."Role ID"; + AccessControl."Company Name" := CopyStr(Company, 1, MaxStrLen(AccessControl."Company Name")); + AccessControl.Scope := AggregatePermissionSet.Scope; + AccessControl."App ID" := AggregatePermissionSet."App ID"; + AccessControl.Insert(); + + exit(UserSecurityId); + end; + + local procedure CreateSuperUser(): Guid + var + AccessControl: Record "Access Control"; + UserSecurityId: Guid; + begin + UserSecurityId := CreateGuid(); + + AccessControl.Init(); + AccessControl."User Security ID" := UserSecurityId; + AccessControl."Role ID" := 'SUPER'; + AccessControl.Scope := AccessControl.Scope::System; + AccessControl.Insert(); + + exit(UserSecurityId); + end; + local procedure InitializeExtensions() var NAVAppInstalledApp: Record "NAV App Installed App"; begin + if not ExtensionManagementTestBound then begin + BindSubscription(ExtensionManagementTest); + ExtensionManagementTestBound := true; + end; + if NAVAppInstalledApp.Get(MainAppId) then ExtensionManagement.UninstallExtension(NAVAppInstalledApp."Package ID", false); if NAVAppInstalledApp.Get(DependingAppId) then ExtensionManagement.UninstallExtension(NAVAppInstalledApp."Package ID", false); end; + [Test] + [TransactionModel(TransactionModel::AutoRollback)] + procedure NonAdminCannotManageExtensions() + var + UserSecurityId: Guid; + begin + UserSecurityId := CreateUserWithPermissionSet('Ext. Mgt. Nonadmin', ''); + + Assert.IsFalse(ExtensionInstallationImplTest.CanManageExtensions(UserSecurityId), 'A non-admin permission set must not grant extension management permission.'); + end; + + [Test] + [TransactionModel(TransactionModel::AutoRollback)] + procedure NonAdminCannotPassExtensionPermissionCheck() + var + UserSecurityId: Guid; + begin + UserSecurityId := CreateUserWithPermissionSet('Ext. Mgt. Nonadmin', ''); + + asserterror ExtensionInstallationImplTest.CheckPermissions(UserSecurityId); + + Assert.ExpectedError(NotSufficientPermissionErr); + end; + + [Test] + [TransactionModel(TransactionModel::AutoRollback)] + procedure ExtensionManagementAdminCanManageExtensions() + var + UserSecurityId: Guid; + begin + UserSecurityId := CreateUserWithPermissionSet('Exten. Mgt. - Admin', ''); + + Assert.IsTrue(ExtensionInstallationImplTest.CanManageExtensions(UserSecurityId), 'Exten. Mgt. - Admin must grant extension management permission.'); + end; + + [Test] + [TransactionModel(TransactionModel::AutoRollback)] + procedure SuperCanManageExtensions() + var + UserSecurityId: Guid; + begin + UserSecurityId := CreateSuperUser(); + + Assert.IsTrue(ExtensionInstallationImplTest.CanManageExtensions(UserSecurityId), 'SUPER must grant extension management permission.'); + end; + + + [Test] + [TransactionModel(TransactionModel::AutoRollback)] + procedure CompanyScopedExtensionManagementAdminCannotManageExtensions() + var + UserSecurityId: Guid; + begin + UserSecurityId := CreateUserWithPermissionSet('Exten. Mgt. - Admin', CompanyName()); + + Assert.IsFalse(ExtensionInstallationImplTest.CanManageExtensions(UserSecurityId), 'Company-scoped Exten. Mgt. - Admin must not grant tenant-wide extension management permission.'); + end; + [EventSubscriber(ObjectType::Codeunit, Codeunit::"Extension Installation Impl", OnCanManageExtensions, '', false, false)] + local procedure AuthorizeTestSession(var Result: Boolean) + begin + Result := true; + end; + [Test] [Scope('OnPrem')] procedure InstallUninstallExtension() @@ -571,4 +683,3 @@ codeunit 133100 "Extension Management Test" Reply := true; end; } -