基于资源授权的最佳实践

This commit is contained in:
hello 2024-04-03 20:39:03 +08:00
parent 65925ce6ef
commit 9fdac9de5f
11 changed files with 72 additions and 110 deletions

View File

@ -9,38 +9,16 @@ using Microsoft.EntityFrameworkCore;
[Route("api/[controller]")] [Route("api/[controller]")]
[ApiController] [ApiController]
[Authorize] [Authorize]
public class PermissionsController(IdentityServiceDbContext dbContext) : ControllerBase public class PermissionsController(IPermissionChecker permissionChecker) : ControllerBase
{ {
[HttpHead]
[HttpGet(nameof(PermissionList))] public async Task<IActionResult> CheckPermission(string permissionName, string? resourceType = null, string? resourceId = null)
public async Task<ActionResult<IEnumerable<PermissionGrantedResponse>>> PermissionList(int? roleId, string? name, string? resourceType = null, string? resourceId = null)
{ {
var roleIds = HttpContext.User.FindAll(CustomClaimTypes.RoleIdentifier).Select(c => Convert.ToInt32(c.Value)); if(await permissionChecker.IsGrantedAsync(permissionName, resourceType, resourceId))
if (roleId.HasValue && roleIds.Any(x => x == roleId))
{ {
roleIds = [roleId.Value]; return Ok();
} }
List<PermissionGrantedResponse> result = []; return Forbid();
IQueryable<PermissionGranted> queryable = dbContext.Set<PermissionGranted>().Where(x => roleIds.Contains(x.RoleId));
if (!string.IsNullOrWhiteSpace(name))
{
queryable = queryable.Where(x => x.PermissionName == name);
}
var permissionGrants = await queryable.Where(x => x.ResourceType == resourceType && x.ResourceId == resourceId).Distinct().ToListAsync();
result.AddRange(permissionGrants.Select(x => new PermissionGrantedResponse
{
Name = x.PermissionName,
ResourceType = x.ResourceType,
ResourceId = x.ResourceId,
IsGranted = true
}));
return Ok(result);
} }
} }

View File

@ -21,9 +21,25 @@ namespace HelloShop.IdentityService.Controllers
[HttpGet("{id}")] [HttpGet("{id}")]
[Authorize(IdentityPermissions.Users.Default)] [Authorize(IdentityPermissions.Users.Default)]
public User? GetUser(int id) public async Task<ActionResult<User>> GetUser(int id, [FromServices] IAuthorizationService authorizationService)
{ {
return dbContext.Set<User>().Find(id); ResourceInfo resource = new(nameof(User), id.ToString());
var authorizationResult = await authorizationService.AuthorizeAsync(User, resource, IdentityPermissions.Users.Default);
if (!authorizationResult.Succeeded)
{
return Forbid();
}
User? user = dbContext.Set<User>().Find(id);
if (user == null)
{
return NotFound();
}
return Ok(user);
} }
[HttpPost] [HttpPost]

View File

@ -2,64 +2,57 @@
using HelloShop.IdentityService.EntityFrameworks; using HelloShop.IdentityService.EntityFrameworks;
using HelloShop.ServiceDefaults.Infrastructure; using HelloShop.ServiceDefaults.Infrastructure;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
namespace HelloShop.IdentityService.DataSeeding namespace HelloShop.IdentityService.DataSeeding
{ {
public class UserDataSeedingProvider(UserManager<User> userManager, RoleManager<Role> roleManager) : IDataSeedingProvider public class UserDataSeedingProvider(UserManager<User> userManager, RoleManager<Role> roleManager) : IDataSeedingProvider
{ {
public async Task SeedingAsync(IServiceProvider serviceProvider) public async Task SeedingAsync(IServiceProvider ServiceProvider)
{ {
var adminRole = await roleManager.FindByNameAsync("AdminRole"); var adminRole = await roleManager.Roles.SingleOrDefaultAsync(x => x.Name == "AdminRole");
if (adminRole == null) if (adminRole == null)
{ {
await roleManager.CreateAsync(new Role adminRole = new Role { Name = "AdminRole", };
{ await roleManager.CreateAsync(adminRole);
Name = "AdminRole"
});
} }
var guestRole = await roleManager.FindByNameAsync("GuestRole"); var guestRole = await roleManager.Roles.SingleOrDefaultAsync(x => x.Name == "GuestRole");
if (guestRole == null) if (guestRole == null)
{ {
await roleManager.CreateAsync(new Role guestRole = new Role { Name = "GuestRole", };
{ await roleManager.CreateAsync(guestRole);
Name = "GuestRole"
});
} }
var adminUser = await userManager.FindByNameAsync("admin"); var adminUser = await userManager.FindByNameAsync("admin");
if (adminUser == null) if (adminUser == null)
{ {
await userManager.CreateAsync(new User adminUser = new User
{ {
UserName = "admin", UserName = "admin",
Email = "admin@test.com" Email = "admin@test.com"
},"admin"); };
await userManager.CreateAsync(adminUser, adminUser.UserName);
} }
if (adminUser!=null)
{
await userManager.AddToRolesAsync(adminUser, ["AdminRole", "GuestRole"]); await userManager.AddToRolesAsync(adminUser, ["AdminRole", "GuestRole"]);
}
var guestUser = await userManager.FindByNameAsync("guest"); var guestUser = await userManager.FindByNameAsync("guest");
if (guestUser == null) if (guestUser == null)
{ {
await userManager.CreateAsync(new User guestUser = new User
{ {
UserName = "guest", UserName = "guest",
Email = "guest@test.com" Email = "guest@test.com"
},"guest"); };
await userManager.CreateAsync(guestUser, guestUser.UserName);
} }
if (guestUser!=null)
{
await userManager.AddToRoleAsync(guestUser, "GuestRole"); await userManager.AddToRoleAsync(guestUser, "GuestRole");
} }
} }
}
} }

View File

@ -12,7 +12,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace HelloShop.IdentityService.EntityFrameworks.Migrations namespace HelloShop.IdentityService.EntityFrameworks.Migrations
{ {
[DbContext(typeof(IdentityServiceDbContext))] [DbContext(typeof(IdentityServiceDbContext))]
[Migration("20240330112954_InitialCreate")] [Migration("20240403122821_InitialCreate")]
partial class InitialCreate partial class InitialCreate
{ {
/// <inheritdoc /> /// <inheritdoc />

View File

@ -4,7 +4,7 @@ namespace HelloShop.ServiceDefaults.Authorization;
public interface IPermissionChecker public interface IPermissionChecker
{ {
Task<bool> IsGrantedAsync(string name, string? resourceType = null, string? resourceId = null); Task<bool> IsGrantedAsync(string permissionName, string? resourceType = null, string? resourceId = null);
Task<bool> IsGrantedAsync(ClaimsPrincipal claimsPrincipal, string name, string? resourceType = null, string? resourceId = null); Task<bool> IsGrantedAsync(ClaimsPrincipal claimsPrincipal, string permissionName, string? resourceType = null, string? resourceId = null);
} }

View File

@ -9,15 +9,15 @@ public abstract class PermissionChecker(IHttpContextAccessor httpContextAccessor
{ {
protected HttpContext HttpContext { get; init; } = httpContextAccessor.HttpContext ?? throw new InvalidOperationException(); protected HttpContext HttpContext { get; init; } = httpContextAccessor.HttpContext ?? throw new InvalidOperationException();
public async Task<bool> IsGrantedAsync(string name, string? resourceType = null, string? resourceId = null) => await IsGrantedAsync(HttpContext.User, name, resourceType, resourceId); public async Task<bool> IsGrantedAsync(string permissionName, string? resourceType = null, string? resourceId = null) => await IsGrantedAsync(HttpContext.User, permissionName, resourceType, resourceId);
public async Task<bool> IsGrantedAsync(ClaimsPrincipal claimsPrincipal, string name, string? resourceType = null, string? resourceId = null) public async Task<bool> IsGrantedAsync(ClaimsPrincipal claimsPrincipal, string permissionName, string? resourceType = null, string? resourceId = null)
{ {
var roleIds = claimsPrincipal.FindAll(CustomClaimTypes.RoleIdentifier).Select(c => Convert.ToInt32(c.Value)).ToArray(); var roleIds = claimsPrincipal.FindAll(CustomClaimTypes.RoleIdentifier).Select(c => Convert.ToInt32(c.Value)).ToArray();
foreach (var roleId in roleIds) foreach (var roleId in roleIds)
{ {
var cacheKey = PermissionGrantCacheItem.CreateCacheKey(roleId, name, resourceType, resourceId); var cacheKey = PermissionGrantCacheItem.CreateCacheKey(roleId, permissionName, resourceType, resourceId);
if (distributedCache.TryGetValue(cacheKey, out PermissionGrantCacheItem? cacheItem) && cacheItem != null) if (distributedCache.TryGetValue(cacheKey, out PermissionGrantCacheItem? cacheItem) && cacheItem != null)
{ {
@ -29,11 +29,11 @@ public abstract class PermissionChecker(IHttpContextAccessor httpContextAccessor
continue; continue;
} }
bool isGranted = await IsGrantedAsync(roleId, name, resourceType, resourceId); bool isGranted = await IsGrantedAsync(roleId, permissionName, resourceType, resourceId);
await distributedCache.SetObjectAsync(cacheKey, new PermissionGrantCacheItem(isGranted), new DistributedCacheEntryOptions await distributedCache.SetObjectAsync(cacheKey, new PermissionGrantCacheItem(isGranted), new DistributedCacheEntryOptions
{ {
AbsoluteExpiration = DateTimeOffset.Now.AddSeconds(1) AbsoluteExpiration = DateTimeOffset.Now
}); });
if (isGranted) if (isGranted)
@ -45,5 +45,5 @@ public abstract class PermissionChecker(IHttpContextAccessor httpContextAccessor
return false; return false;
} }
public abstract Task<bool> IsGrantedAsync(int roleId, string name, string? resourceType = null, string? resourceId = null); public abstract Task<bool> IsGrantedAsync(int roleId, string permissionName, string? resourceType = null, string? resourceId = null);
} }

View File

@ -7,7 +7,21 @@ public class PermissionRequirementHandler(IPermissionChecker permissionChecker)
{ {
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, OperationAuthorizationRequirement requirement) protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, OperationAuthorizationRequirement requirement)
{ {
if (await permissionChecker.IsGrantedAsync(context.User, requirement.Name)) if (context.Resource is IAuthorizationResource resource)
{
if (await permissionChecker.IsGrantedAsync(context.User,requirement.Name, resource.ResourceType, resource.ResourceId))
{
context.Succeed(requirement);
}
else
{
context.Fail();
}
return;
}
if(await permissionChecker.IsGrantedAsync(context.User,requirement.Name))
{ {
context.Succeed(requirement); context.Succeed(requirement);
return; return;
@ -16,14 +30,3 @@ public class PermissionRequirementHandler(IPermissionChecker permissionChecker)
context.Fail(); context.Fail();
} }
} }
public class ResourcePermissionRequirementHandler(IPermissionChecker permissionChecker) : AuthorizationHandler<OperationAuthorizationRequirement, IAuthorizationResource>
{
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, OperationAuthorizationRequirement requirement, IAuthorizationResource resource)
{
if (await permissionChecker.IsGrantedAsync(context.User, requirement.Name, resource.ResourceType, resource.ResourceId))
{
context.Succeed(requirement);
}
}
}

View File

@ -13,7 +13,7 @@ public class RemotePermissionChecker(IHttpContextAccessor httpContextAccessor, I
{ {
private readonly RemotePermissionCheckerOptions _remotePermissionCheckerOptions = options.Value; private readonly RemotePermissionCheckerOptions _remotePermissionCheckerOptions = options.Value;
public override async Task<bool> IsGrantedAsync(int roleId, string name, string? resourceType = null, string? resourceId = null) public override async Task<bool> IsGrantedAsync(int roleId, string permissionName, string? resourceType = null, string? resourceId = null)
{ {
string? accessToken = await HttpContext.GetTokenAsync("access_token"); string? accessToken = await HttpContext.GetTokenAsync("access_token");
@ -25,24 +25,17 @@ public class RemotePermissionChecker(IHttpContextAccessor httpContextAccessor, I
Dictionary<string, string?> parameters = new() Dictionary<string, string?> parameters = new()
{ {
{ nameof(roleId), roleId.ToString() }, { nameof(permissionName), permissionName },
{ nameof(name), name },
{ nameof(resourceType) , resourceType }, { nameof(resourceType) , resourceType },
{ nameof(resourceId), resourceId } { nameof(resourceId), resourceId }
}; };
string queryString = QueryHelpers.AddQueryString(string.Empty, parameters); string queryString = QueryHelpers.AddQueryString(string.Empty, parameters);
var permissionGrants = httpClient.GetFromJsonAsAsyncEnumerable<PermissionGrantedResponse>(queryString); HttpRequestMessage request = new(HttpMethod.Head, queryString);
await foreach (var permissionGrant in permissionGrants) HttpResponseMessage response = await httpClient.SendAsync(request);
{
if (permissionGrant != null && permissionGrant.IsGranted)
{
return true;
}
}
return false; return response.IsSuccessStatusCode;
} }
} }

View File

@ -1,27 +1,7 @@
namespace HelloShop.ServiceDefaults.Authorization; namespace HelloShop.ServiceDefaults.Authorization;
public class ResourceInfo : IAuthorizationResource public record struct ResourceInfo(string ResourceType, string ResourceId) : IAuthorizationResource
{ {
public required string ResourceType { get; set; } public override readonly string ToString() => $"{ResourceType}:{ResourceId}";
public required string ResourceId { get; set; }
public static implicit operator string(ResourceInfo resource) => resource.ToString();
public static explicit operator ResourceInfo(string resourcePath)
{
string[] separators = resourcePath.Split(":");
if (separators == null || separators.Length != 2)
{
throw new ArgumentException("Resource path must be in the format 'type:id'", nameof(resourcePath));
}
ResourceInfo resourceInfo = new() { ResourceType = separators.First(), ResourceId = separators.Last() };
return resourceInfo;
}
public override string ToString() => $"{ResourceType}:{ResourceId}";
} }

View File

@ -78,7 +78,6 @@ public static class PermissionExtensions
{ {
services.AddSingleton<IAuthorizationPolicyProvider, CustomAuthorizationPolicyProvider>(); services.AddSingleton<IAuthorizationPolicyProvider, CustomAuthorizationPolicyProvider>();
services.AddTransient<IAuthorizationHandler, PermissionRequirementHandler>(); services.AddTransient<IAuthorizationHandler, PermissionRequirementHandler>();
services.AddTransient<IAuthorizationHandler, ResourcePermissionRequirementHandler>();
return services; return services;
} }