diff --git a/src/HelloShop.IdentityService/Controllers/TestsController.cs b/src/HelloShop.IdentityService/Controllers/TestsController.cs deleted file mode 100644 index 958ddae..0000000 --- a/src/HelloShop.IdentityService/Controllers/TestsController.cs +++ /dev/null @@ -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 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!"); - } - } -} diff --git a/src/HelloShop.IdentityService/Controllers/UsersController.cs b/src/HelloShop.IdentityService/Controllers/UsersController.cs index 3a49b07..f684e3a 100644 --- a/src/HelloShop.IdentityService/Controllers/UsersController.cs +++ b/src/HelloShop.IdentityService/Controllers/UsersController.cs @@ -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 Get() + [Authorize(IdentityPermissions.Users.Default)] + public IEnumerable GetUsers() { return dbContext.Set(); } - // GET api//5 [HttpGet("{id}")] - public User? Get(int id) + [Authorize(IdentityPermissions.Users.Default)] + public User? GetUser(int id) { return dbContext.Set().Find(id); } - // POST api/ [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().Find(id); + if (user != null) + { + dbContext.SaveChanges(); + } + } + + [HttpDelete("{id}")] + [Authorize(IdentityPermissions.Users.Delete)] + public async Task DeleteUser(int id, [FromServices] IAuthorizationService authorizationService) + { + var user = dbContext.Set().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!"); + } } } diff --git a/src/HelloShop.IdentityService/Entities/User.cs b/src/HelloShop.IdentityService/Entities/User.cs index cd5952d..028b7c3 100644 --- a/src/HelloShop.IdentityService/Entities/User.cs +++ b/src/HelloShop.IdentityService/Entities/User.cs @@ -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 + public class User : IdentityUser, IAuthorizationResource { public DateTimeOffset CreationTime { get; init; } = TimeProvider.System.GetUtcNow(); } diff --git a/src/HelloShop.IdentityService/EntityFrameworks/Migrations/20240327110650_InitialCreate.Designer.cs b/src/HelloShop.IdentityService/EntityFrameworks/Migrations/20240330112954_InitialCreate.Designer.cs similarity index 99% rename from src/HelloShop.IdentityService/EntityFrameworks/Migrations/20240327110650_InitialCreate.Designer.cs rename to src/HelloShop.IdentityService/EntityFrameworks/Migrations/20240330112954_InitialCreate.Designer.cs index f7b807a..ea30e08 100644 --- a/src/HelloShop.IdentityService/EntityFrameworks/Migrations/20240327110650_InitialCreate.Designer.cs +++ b/src/HelloShop.IdentityService/EntityFrameworks/Migrations/20240330112954_InitialCreate.Designer.cs @@ -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 { /// diff --git a/src/HelloShop.IdentityService/EntityFrameworks/Migrations/20240327110650_InitialCreate.cs b/src/HelloShop.IdentityService/EntityFrameworks/Migrations/20240330112954_InitialCreate.cs similarity index 100% rename from src/HelloShop.IdentityService/EntityFrameworks/Migrations/20240327110650_InitialCreate.cs rename to src/HelloShop.IdentityService/EntityFrameworks/Migrations/20240330112954_InitialCreate.cs diff --git a/src/HelloShop.IdentityService/Program.cs b/src/HelloShop.IdentityService/Program.cs index 225d88c..91d0980 100644 --- a/src/HelloShop.IdentityService/Program.cs +++ b/src/HelloShop.IdentityService/Program.cs @@ -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().AddCustomAuthorization(); var app = builder.Build(); diff --git a/src/HelloShop.ServiceDefaults/Authorization/CustomAuthorizationPolicyProvider.cs b/src/HelloShop.ServiceDefaults/Authorization/CustomAuthorizationPolicyProvider.cs new file mode 100644 index 0000000..846fba4 --- /dev/null +++ b/src/HelloShop.ServiceDefaults/Authorization/CustomAuthorizationPolicyProvider.cs @@ -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 options, IPermissionDefinitionManager permissionDefinitionManager) : DefaultAuthorizationPolicyProvider(options) +{ + public override async Task 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; + } +} diff --git a/src/HelloShop.ServiceDefaults/Authorization/IAuthorizationResource.cs b/src/HelloShop.ServiceDefaults/Authorization/IAuthorizationResource.cs new file mode 100644 index 0000000..02804b3 --- /dev/null +++ b/src/HelloShop.ServiceDefaults/Authorization/IAuthorizationResource.cs @@ -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(); +} \ No newline at end of file diff --git a/src/HelloShop.ServiceDefaults/Authorization/PermissionChecker.cs b/src/HelloShop.ServiceDefaults/Authorization/PermissionChecker.cs index dc9fe9a..5d10d6d 100644 --- a/src/HelloShop.ServiceDefaults/Authorization/PermissionChecker.cs +++ b/src/HelloShop.ServiceDefaults/Authorization/PermissionChecker.cs @@ -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) diff --git a/src/HelloShop.ServiceDefaults/Authorization/PermissionRequirementHandler.cs b/src/HelloShop.ServiceDefaults/Authorization/PermissionRequirementHandler.cs new file mode 100644 index 0000000..f6aaa43 --- /dev/null +++ b/src/HelloShop.ServiceDefaults/Authorization/PermissionRequirementHandler.cs @@ -0,0 +1,29 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Authorization.Infrastructure; + +namespace HelloShop.ServiceDefaults.Authorization; + +public class PermissionRequirementHandler(IPermissionChecker permissionChecker) : AuthorizationHandler +{ + 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 +{ + 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); + } + } +} diff --git a/src/HelloShop.ServiceDefaults/Authorization/ResourceInfo.cs b/src/HelloShop.ServiceDefaults/Authorization/ResourceInfo.cs new file mode 100644 index 0000000..cb94bfb --- /dev/null +++ b/src/HelloShop.ServiceDefaults/Authorization/ResourceInfo.cs @@ -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}"; +} diff --git a/src/HelloShop.ServiceDefaults/Constants/EntityConnstants.cs b/src/HelloShop.ServiceDefaults/Constants/EntityConnstants.cs new file mode 100644 index 0000000..a830f80 --- /dev/null +++ b/src/HelloShop.ServiceDefaults/Constants/EntityConnstants.cs @@ -0,0 +1,6 @@ +namespace HelloShop.ServiceDefaults.Constants; + +public static class EntityConnstants +{ + public const string DefaultKey = "Id"; +} diff --git a/src/HelloShop.ServiceDefaults/Extensions/PermissionExtensions.cs b/src/HelloShop.ServiceDefaults/Extensions/PermissionExtensions.cs index 0d68f8b..e5abec2 100644 --- a/src/HelloShop.ServiceDefaults/Extensions/PermissionExtensions.cs +++ b/src/HelloShop.ServiceDefaults/Extensions/PermissionExtensions.cs @@ -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(); + services.AddTransient(); + services.AddTransient(); + + return services; + } }