基于资源授权的最佳实践
This commit is contained in:
parent
65925ce6ef
commit
9fdac9de5f
@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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]
|
||||||
|
@ -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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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 />
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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}";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user