基于策略和资源的授权机制

This commit is contained in:
hello 2024-03-30 21:08:58 +08:00
parent 2d0e504108
commit 65925ce6ef
13 changed files with 172 additions and 51 deletions

View File

@ -1,39 +0,0 @@
// Copyright (c) HelloShop Corporation. All rights reserved.
// See the license file in the project root for more information.
using HelloShop.ServiceDefaults.Authorization;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace HelloShop.IdentityService.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class TestsController(IPermissionChecker permissionChecker, IAuthorizationService authorizationService) : ControllerBase
{
[Authorize(IdentityPermissions.Users.Update)]
public async Task<IActionResult> Foo()
{
var result = await permissionChecker.IsGrantedAsync(IdentityPermissions.Users.Update, "Order", "1");
var result2 = await authorizationService.AuthorizeAsync(User, IdentityPermissions.Users.Update);
return Ok("Hello, World!");
}
[HttpGet(nameof(Bar))]
[Authorize(IdentityPermissions.Users.Create)]
public IActionResult Bar()
{
return Ok("Hello, World!");
}
[Authorize(IdentityPermissions.Users.Delete)]
[HttpGet(nameof(Baz))]
public IActionResult Baz()
{
return Ok("Hello, World!");
}
}
}

View File

@ -1,5 +1,7 @@
using HelloShop.IdentityService.Entities;
using HelloShop.IdentityService.EntityFrameworks;
using HelloShop.ServiceDefaults.Authorization;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
// For more information on enabling Web API for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860
@ -11,24 +13,66 @@ namespace HelloShop.IdentityService.Controllers
public class UsersController(IdentityServiceDbContext dbContext) : ControllerBase
{
[HttpGet]
public IEnumerable<User> Get()
[Authorize(IdentityPermissions.Users.Default)]
public IEnumerable<User> GetUsers()
{
return dbContext.Set<User>();
}
// GET api/<UsersController>/5
[HttpGet("{id}")]
public User? Get(int id)
[Authorize(IdentityPermissions.Users.Default)]
public User? GetUser(int id)
{
return dbContext.Set<User>().Find(id);
}
// POST api/<UsersController>
[HttpPost]
public void Post([FromBody] User value)
[Authorize(IdentityPermissions.Users.Create)]
public void PostUser([FromBody] User value)
{
dbContext.Add(value);
dbContext.SaveChanges();
}
[HttpPut("{id}")]
[Authorize(IdentityPermissions.Users.Update)]
public void PutUser(int id, [FromBody] User value)
{
var user = dbContext.Set<User>().Find(id);
if (user != null)
{
dbContext.SaveChanges();
}
}
[HttpDelete("{id}")]
[Authorize(IdentityPermissions.Users.Delete)]
public async Task<IActionResult> DeleteUser(int id, [FromServices] IAuthorizationService authorizationService)
{
var user = dbContext.Set<User>().Find(id);
if (user != null)
{
var result = await authorizationService.AuthorizeAsync(User, user, IdentityPermissions.Users.Delete);
if (result.Succeeded)
{
dbContext.Remove(user);
dbContext.SaveChanges();
return Ok();
}
}
return Unauthorized();
}
[HttpGet(nameof(Bar))]
[Authorize(IdentityPermissions.Users.Create)]
public IActionResult Bar()
{
return Ok("Hello, World!");
}
}
}

View File

@ -1,11 +1,13 @@
// Copyright (c) HelloShop Corporation. All rights reserved.
// See the license file in the project root for more information.
using HelloShop.ServiceDefaults.Authorization;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
namespace HelloShop.IdentityService.Entities
{
public class User: IdentityUser<int>
public class User : IdentityUser<int>, IAuthorizationResource
{
public DateTimeOffset CreationTime { get; init; } = TimeProvider.System.GetUtcNow();
}

View File

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

View File

@ -56,10 +56,7 @@ builder.Services.AddAuthentication(options =>
builder.Services.AddDataSeedingProviders();
builder.Services.AddOpenApi();
builder.Services.AddPermissionDefinitions();
builder.Services.AddHttpClient().AddHttpContextAccessor().AddRemotePermissionChecker(options =>
{
options.ApiEndpoint = "https://localhost:5001/api/Permissions/PermissionList";
});
builder.Services.AddAuthorization().AddDistributedMemoryCache().AddHttpClient().AddHttpContextAccessor().AddTransient<IPermissionChecker, LocalPermissionChecker>().AddCustomAuthorization();
var app = builder.Build();

View File

@ -0,0 +1,32 @@
using HelloShop.ServiceDefaults.Permissions;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Infrastructure;
using Microsoft.Extensions.Options;
namespace HelloShop.ServiceDefaults.Authorization;
public class CustomAuthorizationPolicyProvider(IOptions<AuthorizationOptions> options, IPermissionDefinitionManager permissionDefinitionManager) : DefaultAuthorizationPolicyProvider(options)
{
public override async Task<AuthorizationPolicy?> GetPolicyAsync(string policyName)
{
AuthorizationPolicy? policy = await base.GetPolicyAsync(policyName);
if (policy != null)
{
return policy;
}
var permissionDefinition = permissionDefinitionManager.GetOrNullAsync(policyName);
if (permissionDefinition != null)
{
var policyBuilder = new AuthorizationPolicyBuilder();
policyBuilder.Requirements.Add(new OperationAuthorizationRequirement { Name = policyName });
return policyBuilder.Build();
}
return null;
}
}

View File

@ -0,0 +1,13 @@
using HelloShop.ServiceDefaults.Constants;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Reflection.Metadata.Ecma335;
namespace HelloShop.ServiceDefaults.Authorization;
public interface IAuthorizationResource
{
string ResourceType => GetType().Name;
string ResourceId => GetType().GetProperty(EntityConnstants.DefaultKey)?.GetValue(this)?.ToString() ?? throw new NotImplementedException();
}

View File

@ -33,7 +33,7 @@ public abstract class PermissionChecker(IHttpContextAccessor httpContextAccessor
await distributedCache.SetObjectAsync(cacheKey, new PermissionGrantCacheItem(isGranted), new DistributedCacheEntryOptions
{
SlidingExpiration = TimeSpan.FromMinutes(10)
AbsoluteExpiration = DateTimeOffset.Now.AddSeconds(1)
});
if (isGranted)

View File

@ -0,0 +1,29 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Infrastructure;
namespace HelloShop.ServiceDefaults.Authorization;
public class PermissionRequirementHandler(IPermissionChecker permissionChecker) : AuthorizationHandler<OperationAuthorizationRequirement>
{
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, OperationAuthorizationRequirement requirement)
{
if (await permissionChecker.IsGrantedAsync(context.User, requirement.Name))
{
context.Succeed(requirement);
return;
}
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

@ -0,0 +1,27 @@
namespace HelloShop.ServiceDefaults.Authorization;
public class ResourceInfo : IAuthorizationResource
{
public required string ResourceType { get; set; }
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

@ -0,0 +1,6 @@
namespace HelloShop.ServiceDefaults.Constants;
public static class EntityConnstants
{
public const string DefaultKey = "Id";
}

View File

@ -1,5 +1,6 @@
using HelloShop.ServiceDefaults.Authorization;
using HelloShop.ServiceDefaults.Permissions;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
@ -72,4 +73,13 @@ public static class PermissionExtensions
return services;
}
public static IServiceCollection AddCustomAuthorization(this IServiceCollection services)
{
services.AddSingleton<IAuthorizationPolicyProvider, CustomAuthorizationPolicyProvider>();
services.AddTransient<IAuthorizationHandler, PermissionRequirementHandler>();
services.AddTransient<IAuthorizationHandler, ResourcePermissionRequirementHandler>();
return services;
}
}