fix blocker 1 - super admin

This commit is contained in:
hamid
2026-08-02 21:03:26 +03:30
parent 42f38f5a72
commit 2cd1075286
6 changed files with 148 additions and 63 deletions
@@ -1,4 +1,5 @@
using System.Security.Claims;
using Baya.Domain.Entities.User;
namespace Baya.Infrastructure.Identity.Identity.PermissionManager;
@@ -6,18 +7,17 @@ public class DynamicPermissionService : IDynamicPermissionService
{
public bool CanAccess(ClaimsPrincipal user, string area, string controller, string action)
{
if (user.IsInRole("admin"))
// super_admin is definitionally the top role, but RoleNames has no hierarchy — each admin
// sub-role is a sibling, so it must be granted the bypass explicitly, not implied by "admin".
if (user.IsInRole(RoleNames.Admin) || user.IsInRole(RoleNames.SuperAdmin))
{
return true;
}
var key = $"{area}:{controller}:";
var userClaims = user.FindAll(ConstantPolicies.DynamicPermission);
return userClaims.Any(item => item.Value.Equals(key, StringComparison.OrdinalIgnoreCase));
}
}
@@ -1,5 +1,8 @@
using Baya.Application.Contracts.Identity;
using Baya.Application.Models.Identity;
using Baya.Domain.Entities.User;
using Baya.Infrastructure.Identity.Identity.Manager;
using Baya.Infrastructure.Identity.Identity.PermissionManager;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
@@ -15,12 +18,25 @@ public class SeedDataBase : ISeedDataBase
private readonly AppUserManager _userManager;
private readonly AppRoleManager _roleManager;
private readonly IConfiguration _configuration;
private readonly IRoleManagerService _roleManagerService;
public SeedDataBase(AppUserManager userManager, AppRoleManager roleManager, IConfiguration configuration)
// The admin-console permission matrix owned by each non-admin sub-role — mirrors
// client/src/hooks/capabilities.ts (useAdminCapabilities), the reviewed source of truth for who
// owns which console. Area is always empty: no controller uses [Area] today.
private static readonly IReadOnlyDictionary<string, string[]> AdminSubRolePermissions =
new Dictionary<string, string[]>
{
[RoleNames.Finance] = ["AdminRefunds", "AdminPayouts", "PlatformConfig"],
[RoleNames.Support] = ["AdminVerifications", "AdminVerificationStepTypes", "SupportAlerts", "AdminTickets"],
[RoleNames.Moderation] = ["AdminReviews", "Reviews"],
};
public SeedDataBase(AppUserManager userManager, AppRoleManager roleManager, IConfiguration configuration, IRoleManagerService roleManagerService)
{
_userManager = userManager;
_roleManager = roleManager;
_configuration = configuration;
_roleManagerService = roleManagerService;
}
public async Task Seed()
@@ -38,9 +54,39 @@ public class SeedDataBase : ISeedDataBase
}
}
await SeedAdminSubRolePermissionsAsync();
await SeedBootstrapAdminAsync();
}
// finance/support/moderation are sibling roles, not implied by super_admin — each needs its own
// DynamicPermission claims scoped to the consoles it owns. Skips roles already at the target claim
// set so a normal restart doesn't churn every affected user's security stamp.
private async Task SeedAdminSubRolePermissionsAsync()
{
foreach (var (roleName, controllers) in AdminSubRolePermissions)
{
var role = await _roleManager.FindByNameAsync(roleName);
if (role == null)
continue;
var targetPermissions = controllers.Select(controller => $":{controller}:").ToList();
var existingPermissions = (await _roleManager.GetClaimsAsync(role))
.Where(c => c.Type == ConstantPolicies.DynamicPermission)
.Select(c => c.Value)
.ToHashSet(StringComparer.OrdinalIgnoreCase);
if (existingPermissions.SetEquals(targetPermissions))
continue;
await _roleManagerService.ChangeRolePermissionsAsync(new EditRolePermissionsDto
{
RoleId = role.Id,
Permissions = targetPermissions
});
}
}
// The bootstrap admin is config-driven, never a committed credential: it is created only when both
// Seed:AdminUsername and Seed:AdminPassword are supplied (via the environment-specific appsettings file, environment
// variables in a deployment). With neither configured — the default for Testing and any fresh boot —
@@ -0,0 +1,54 @@
using System.Security.Claims;
using Baya.Domain.Entities.User;
using Baya.Infrastructure.Identity.Identity.PermissionManager;
namespace Baya.Test.Infrastructure.Identity
{
public class DynamicPermissionServiceTest
{
private readonly DynamicPermissionService _sut = new();
[Fact]
public void Admin_Role_Bypasses_Permission_Check()
{
var user = BuildUser(RoleNames.Admin);
Assert.True(_sut.CanAccess(user, area: null, controller: "AdminRefunds", action: "Create"));
}
[Fact]
public void SuperAdmin_Role_Bypasses_Permission_Check()
{
var user = BuildUser(RoleNames.SuperAdmin);
Assert.True(_sut.CanAccess(user, area: null, controller: "AdminRefunds", action: "Create"));
}
[Fact]
public void Role_With_Matching_Claim_Is_Granted_Access()
{
var user = BuildUser(RoleNames.Finance, dynamicPermission: ":AdminRefunds:");
Assert.True(_sut.CanAccess(user, area: null, controller: "AdminRefunds", action: "Create"));
}
[Fact]
public void Role_Without_Matching_Claim_Is_Denied_Access()
{
var user = BuildUser(RoleNames.Finance, dynamicPermission: ":AdminRefunds:");
Assert.False(_sut.CanAccess(user, area: null, controller: "AdminReviews", action: "Status"));
}
private static ClaimsPrincipal BuildUser(string role, string? dynamicPermission = null)
{
var claims = new List<Claim> { new(ClaimTypes.Role, role) };
if (dynamicPermission != null)
claims.Add(new Claim(ConstantPolicies.DynamicPermission, dynamicPermission));
var identity = new ClaimsIdentity(claims, "TestAuth", ClaimTypes.Name, ClaimTypes.Role);
return new ClaimsPrincipal(identity);
}
}
}