Skip to content

Commit

Permalink
Add new filter attribute (AuthorizeMenuAttribute) that allows for ver…
Browse files Browse the repository at this point in the history
…ification of access to a specific controller and action in the admin panel using menu configuration
  • Loading branch information
support committed Oct 8, 2023
1 parent cbec431 commit 080df38
Show file tree
Hide file tree
Showing 9 changed files with 440 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/aspnetcore.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,5 +47,7 @@ jobs:
run: dotnet test ./src/Tests/Grand.Infrastructure.Tests/Grand.Infrastructure.Tests.csproj
- name: Grand.SharedKernel.Tests Unit Tests
run: dotnet test ./src/Tests/Grand.SharedKernel.Tests/Grand.SharedKernel.Tests.csproj
- name: Grand.Web.Common.Tests Unit Tests
run: dotnet test ./src/Tests/Grand.Web.Common.Tests/Grand.Web.Common.Tests.csproj
- name: Grand.Web.Admin.Tests Unit Tests
run: dotnet test ./src/Tests/Grand.Web.Admin.Tests/Grand.Web.Admin.Tests.csproj
2 changes: 2 additions & 0 deletions .github/workflows/grandnode.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ jobs:
run: dotnet test ./src/Tests/Grand.Infrastructure.Tests/Grand.Infrastructure.Tests.csproj
- name: Grand.SharedKernel.Tests Unit Tests
run: dotnet test ./src/Tests/Grand.SharedKernel.Tests/Grand.SharedKernel.Tests.csproj
- name: Grand.Web.Common.Tests Unit Tests
run: dotnet test ./src/Tests/Grand.Web.Common.Tests/Grand.Web.Common.Tests.csproj
- name: Grand.Web.Admin.Tests Unit Tests
run: dotnet test ./src/Tests/Grand.Web.Admin.Tests/Grand.Web.Admin.Tests.csproj
- name: Publish
Expand Down
7 changes: 7 additions & 0 deletions GrandNode.sln
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Grand.Infrastructure.Tests"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Grand.Web.Admin.Tests", "src\Tests\Grand.Web.Admin.Tests\Grand.Web.Admin.Tests.csproj", "{0B2B4B1B-EBF7-447F-9E5F-C55CE3247929}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Grand.Web.Common.Tests", "src\Tests\Grand.Web.Common.Tests\Grand.Web.Common.Tests.csproj", "{6969D83E-567C-47A7-8E7F-7FE40940704E}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -324,6 +326,10 @@ Global
{0B2B4B1B-EBF7-447F-9E5F-C55CE3247929}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0B2B4B1B-EBF7-447F-9E5F-C55CE3247929}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0B2B4B1B-EBF7-447F-9E5F-C55CE3247929}.Release|Any CPU.Build.0 = Release|Any CPU
{6969D83E-567C-47A7-8E7F-7FE40940704E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6969D83E-567C-47A7-8E7F-7FE40940704E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6969D83E-567C-47A7-8E7F-7FE40940704E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6969D83E-567C-47A7-8E7F-7FE40940704E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -377,6 +383,7 @@ Global
{EEEE2033-07A4-42BB-AFEE-3F83E0BD3044} = {6360202A-F931-4BBD-ADBD-C9A628EE59F8}
{50049A49-D27E-4200-9875-F6C486FD608D} = {6360202A-F931-4BBD-ADBD-C9A628EE59F8}
{0B2B4B1B-EBF7-447F-9E5F-C55CE3247929} = {6360202A-F931-4BBD-ADBD-C9A628EE59F8}
{6969D83E-567C-47A7-8E7F-7FE40940704E} = {6360202A-F931-4BBD-ADBD-C9A628EE59F8}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {88B478F4-FD3B-4C24-9E84-4FAAF0254397}
Expand Down
24 changes: 24 additions & 0 deletions src/Core/Grand.SharedKernel/Extensions/ExtendedLinq.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,29 @@ public static bool ContainsAny<TSource>(this IEnumerable<TSource> source, IEnume
from item in sequence
select accseq.Concat(new[] { item }));
}

public static async Task<bool> AllAsync<T>(this IEnumerable<T> source, Func<T, Task<bool>> predicate)
{
foreach (var item in source)
{
if (!await predicate(item))
{
return false;
}
}
return true;
}
public static async Task<bool> AnyAsync<T>(this IEnumerable<T> source, Func<T, Task<bool>> predicate)
{
foreach (var item in source)
{
if (await predicate(item))
{
return true;
}
}
return false;
}

}
}
1 change: 1 addition & 0 deletions src/Tests/Grand.Web.Common.Tests/App_Data/index.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

222 changes: 222 additions & 0 deletions src/Tests/Grand.Web.Common.Tests/AuthorizeMenuAttributeTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
using Grand.Business.Core.Interfaces.Common.Security;
using Grand.Business.Core.Interfaces.System.Admin;
using Grand.Domain.Admin;
using Grand.Domain.Customers;
using Grand.Domain.Data;
using Grand.Infrastructure;
using Grand.Infrastructure.Configuration;
using Grand.SharedKernel.Extensions;
using Grand.Web.Common.Filters;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Routing;
using Moq;
using NUnit.Framework;
using Assert = Microsoft.VisualStudio.TestTools.UnitTesting.Assert;

namespace Grand.Web.Common.Tests;

[TestFixture]
public class AuthorizeMenuAttributeTests
{
private Mock<IPermissionService> _mockPermissionService;
private Mock<IAdminSiteMapService> _mockAdminSiteMapService;
private Mock<IWorkContext> _mockWorkContext;
private SecurityConfig _securityConfig;
private AuthorizationFilterContext _mockFilterContext;

[SetUp]
public void Setup()
{
CommonPath.BaseDirectory = "";
DataSettingsManager.SaveSettings(new DataSettings() { ConnectionString = "connectionstring", DbProvider = DbProvider.MongoDB }).GetAwaiter().GetResult();
var settings = DataSettingsManager.LoadSettings();
_mockPermissionService = new Mock<IPermissionService>();
_mockAdminSiteMapService = new Mock<IAdminSiteMapService>();
_mockWorkContext = new Mock<IWorkContext>();
_securityConfig = new SecurityConfig();
var filters = new List<IFilterMetadata>
{
new AuthorizeMenuAttribute(false)
};
_mockFilterContext = new AuthorizationFilterContext(GetMockedAuthorizationFilterContext(), filters);
}
private AuthorizationFilterContext GetMockedAuthorizationFilterContext()
{

// Mocking HttpContext
var httpContextMock = new Mock<HttpContext>();

// Mocking RouteData
var routeData = new RouteData();
routeData.Values.Add("controller", "SampleController");
routeData.Values.Add("action", "SampleAction");

// Mocking ActionDescriptor
var actionDescriptorMock = new Mock<ActionDescriptor>();
//actionDescriptorMock.Object.Filters.Add(new AuthorizeMenuAttribute(false));


var actionContext = new ActionContext(
httpContextMock.Object,
routeData,
actionDescriptorMock.Object
);

// Mocking filters for the context (can be empty if not needed for testing)
var filters = new List<IFilterMetadata>
{
new AuthorizeMenuAttribute(false)
};

var authorizationFilterContext = new AuthorizationFilterContext(actionContext, filters);

return authorizationFilterContext;
}
[Test]
public async Task TestAuthorizeMenuAttribute_WithoutIgnoreFilter_DatabaseNotInstalled()
{
// Arrange
var attribute = new AuthorizeMenuAttribute(false);
var filter = new AuthorizeMenuAttribute.AuthorizeMenuFilter(
false,
_mockPermissionService.Object,
_mockAdminSiteMapService.Object,
_mockWorkContext.Object,
_securityConfig
);

// Act
await filter.OnAuthorizationAsync(_mockFilterContext);

// Assert
// Assuming RedirectToRouteResult is not set when the database is not installed
Assert.IsNull(_mockFilterContext.Result);
}

[Test]
public async Task TestAuthorizeMenuAttribute_WithoutIgnoreFilter_AuthorizeAdminMenuDisabled()
{
// Arrange
_securityConfig.AuthorizeAdminMenu = false;
var attribute = new AuthorizeMenuAttribute(false);
var filter = new AuthorizeMenuAttribute.AuthorizeMenuFilter(
false,
_mockPermissionService.Object,
_mockAdminSiteMapService.Object,
_mockWorkContext.Object,
_securityConfig
);

// Act
await filter.OnAuthorizationAsync(_mockFilterContext);

// Assert
// Assuming RedirectToRouteResult is not set when AuthorizeAdminMenu is disabled
Assert.IsNull(_mockFilterContext.Result);
}

[Test]
public async Task TestAuthorizeMenuAttribute_WithValidSiteMap_AllPermissions()
{
// Arrange
var menuSiteMap = new AdminSiteMap { AllPermissions = true, PermissionNames = new List<string> { "Permission1" }, ActionName = "SampleAction", ControllerName = "SampleController" };
_mockAdminSiteMapService.Setup(s => s.GetSiteMap()).ReturnsAsync(new List<AdminSiteMap> { menuSiteMap });
_mockPermissionService.Setup(s => s.Authorize(It.IsAny<string>(), It.IsAny<Customer>())).ReturnsAsync(false);
_securityConfig.AuthorizeAdminMenu = true;
var attribute = new AuthorizeMenuAttribute(false);
var filter = new AuthorizeMenuAttribute.AuthorizeMenuFilter(
false,
_mockPermissionService.Object,
_mockAdminSiteMapService.Object,
_mockWorkContext.Object,
_securityConfig
);

// Act
await filter.OnAuthorizationAsync(_mockFilterContext);

// Assert
Assert.IsInstanceOfType(_mockFilterContext.Result, typeof(RedirectToRouteResult));
}

[Test]
public async Task TestAuthorizeMenuAttribute_NoPermissionsInSiteMap()
{
// Arrange
var menuSiteMap = new AdminSiteMap { AllPermissions = true, PermissionNames = new List<string> { }, ActionName = "SampleAction", ControllerName = "SampleController" };
_mockAdminSiteMapService.Setup(s => s.GetSiteMap()).ReturnsAsync(new List<AdminSiteMap> { menuSiteMap });
_mockPermissionService.Setup(s => s.Authorize(It.IsAny<string>(), It.IsAny<Customer>())).ReturnsAsync(false);

_securityConfig.AuthorizeAdminMenu = true;
var attribute = new AuthorizeMenuAttribute(false);
var filter = new AuthorizeMenuAttribute.AuthorizeMenuFilter(
false,
_mockPermissionService.Object,
_mockAdminSiteMapService.Object,
_mockWorkContext.Object,
_securityConfig
);

// Act
await filter.OnAuthorizationAsync(_mockFilterContext);

// Assert
// No permissions in the sitemap, so no redirect should occur
Assert.IsNull(_mockFilterContext.Result);
}

[Test]
public async Task TestAuthorizeMenuAttribute_WithoutAllPermissions_Authorized()
{
// Arrange
var menuSiteMap = new AdminSiteMap { AllPermissions = false, PermissionNames = new List<string> { "Permission1" }, ActionName = "SampleAction", ControllerName = "SampleController" };
_mockAdminSiteMapService.Setup(s => s.GetSiteMap()).ReturnsAsync(new List<AdminSiteMap> { menuSiteMap });
_mockPermissionService.Setup(s => s.Authorize(It.IsAny<string>(), It.IsAny<Customer>())).ReturnsAsync(true);
_securityConfig.AuthorizeAdminMenu = true;

var attribute = new AuthorizeMenuAttribute(false);
var filter = new AuthorizeMenuAttribute.AuthorizeMenuFilter(
false,
_mockPermissionService.Object,
_mockAdminSiteMapService.Object,
_mockWorkContext.Object,
_securityConfig
);

// Act
await filter.OnAuthorizationAsync(_mockFilterContext);

// Assert
// Permission exists and user is authorized, so no redirect should occur
Assert.IsNull(_mockFilterContext.Result);
}

[Test]
public async Task TestAuthorizeMenuAttribute_WithoutAllPermissions_NotAuthorized()
{
// Arrange
var menuSiteMap = new AdminSiteMap { AllPermissions = false, PermissionNames = new List<string> { "Permission1" }, ActionName = "SampleAction", ControllerName = "SampleController" };
_mockAdminSiteMapService.Setup(s => s.GetSiteMap()).ReturnsAsync(new List<AdminSiteMap> { menuSiteMap });
_mockPermissionService.Setup(s => s.Authorize(It.IsAny<string>(), It.IsAny<Customer>())).ReturnsAsync(false);
_securityConfig.AuthorizeAdminMenu = true;

var attribute = new AuthorizeMenuAttribute(false);
var filter = new AuthorizeMenuAttribute.AuthorizeMenuFilter(
false,
_mockPermissionService.Object,
_mockAdminSiteMapService.Object,
_mockWorkContext.Object,
_securityConfig
);

// Act
await filter.OnAuthorizationAsync(_mockFilterContext);

// Assert
// Permission exists but user is not authorized, so a redirect should occur
Assert.IsInstanceOfType(_mockFilterContext.Result, typeof(RedirectToRouteResult));
}
}
35 changes: 35 additions & 0 deletions src/Tests/Grand.Web.Common.Tests/Grand.Web.Common.Tests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<Import Project="..\..\Build\Grand.Common.props" />
<PropertyGroup>
<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
<PackageReference Include="Moq" Version="4.20.69" />
<PackageReference Include="MSTest.TestAdapter" Version="3.1.1" />
<PackageReference Include="MSTest.TestFramework" Version="3.1.1" />
<PackageReference Include="coverlet.collector" Version="6.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="NUnit" Version="3.13.3" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\Core\Grand.SharedKernel\Grand.SharedKernel.csproj" />
<ProjectReference Include="..\..\Web\Grand.Web.Common\Grand.Web.Common.csproj" />
</ItemGroup>

<ItemGroup>
<Folder Include="Properties\" />
</ItemGroup>

<ItemGroup>
<None Remove="Properties\launchSettings.json" />
<None Update="App_Data\index.txt">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
1 change: 1 addition & 0 deletions src/Web/Grand.Web.Admin/Controllers/BaseAdminController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ namespace Grand.Web.Admin.Controllers
[AutoValidateAntiforgeryToken]
[Area(Constants.AreaAdmin)]
[AuthorizeVendor]
[AuthorizeMenu]
public abstract class BaseAdminController : BaseController
{

Expand Down
Loading

0 comments on commit 080df38

Please sign in to comment.