diff --git a/src/HelloShop.IdentityService/Authorization/LocalPermissionChecker.cs b/src/HelloShop.IdentityService/Authorization/LocalPermissionChecker.cs new file mode 100644 index 0000000..0ef87bf --- /dev/null +++ b/src/HelloShop.IdentityService/Authorization/LocalPermissionChecker.cs @@ -0,0 +1,14 @@ +using HelloShop.IdentityService.Entities; +using HelloShop.IdentityService.EntityFrameworks; +using HelloShop.ServiceDefaults.Authorization; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Caching.Distributed; + +namespace HelloShop.IdentityService.Authorization; +public class LocalPermissionChecker(IHttpContextAccessor httpContextAccessor, IdentityServiceDbContext dbContext, IDistributedCache distributedCache) : PermissionChecker(httpContextAccessor, distributedCache) +{ + public override async Task IsGrantedAsync(int roleId, string name, string? resourceType = null, string? resourceId = null) + { + return await dbContext.Set().AsNoTracking().AnyAsync(x => x.RoleId == roleId && x.PermissionName == name && x.ResourceType == resourceType && x.ResourceId == resourceId); + } +} \ No newline at end of file diff --git a/src/HelloShop.IdentityService/Controllers/PermissionsController.cs b/src/HelloShop.IdentityService/Controllers/PermissionsController.cs new file mode 100644 index 0000000..2f066ba --- /dev/null +++ b/src/HelloShop.IdentityService/Controllers/PermissionsController.cs @@ -0,0 +1,46 @@ +using HelloShop.IdentityService.Entities; +using HelloShop.IdentityService.EntityFrameworks; +using HelloShop.ServiceDefaults.Authorization; +using HelloShop.ServiceDefaults.Constants; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +[Route("api/[controller]")] +[ApiController] +[Authorize] +public class PermissionsController(IdentityServiceDbContext dbContext) : ControllerBase +{ + + [HttpGet(nameof(PermissionList))] + public async Task>> 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 (roleId.HasValue && roleIds.Any(x => x == roleId)) + { + roleIds = [roleId.Value]; + } + + List result = []; + + IQueryable queryable = dbContext.Set().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); + } +} diff --git a/src/HelloShop.IdentityService/Controllers/TestsController.cs b/src/HelloShop.IdentityService/Controllers/TestsController.cs index d1cfe20..958ddae 100644 --- a/src/HelloShop.IdentityService/Controllers/TestsController.cs +++ b/src/HelloShop.IdentityService/Controllers/TestsController.cs @@ -1,20 +1,24 @@ // 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.Http; using Microsoft.AspNetCore.Mvc; namespace HelloShop.IdentityService.Controllers { [Route("api/[controller]")] [ApiController] - public class TestsController : ControllerBase + public class TestsController(IPermissionChecker permissionChecker, IAuthorizationService authorizationService) : ControllerBase { - [HttpGet(nameof(Foo))] + [Authorize(IdentityPermissions.Users.Update)] - public IActionResult Foo() + 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!"); } diff --git a/src/HelloShop.IdentityService/Entities/PermissionGranted.cs b/src/HelloShop.IdentityService/Entities/PermissionGranted.cs new file mode 100644 index 0000000..e3634a1 --- /dev/null +++ b/src/HelloShop.IdentityService/Entities/PermissionGranted.cs @@ -0,0 +1,14 @@ +namespace HelloShop.IdentityService.Entities; + +public class PermissionGranted +{ + public int Id { get; set; } + + public int RoleId { get; set; } + + public required string PermissionName { get; set; } + + public string? ResourceType { get; set; } + + public string? ResourceId { get; set; } +} diff --git a/src/HelloShop.IdentityService/EntityFrameworks/EntityConfigurations/PermissionGrantedEntityTypeConfiguration.cs b/src/HelloShop.IdentityService/EntityFrameworks/EntityConfigurations/PermissionGrantedEntityTypeConfiguration.cs new file mode 100644 index 0000000..20c55a5 --- /dev/null +++ b/src/HelloShop.IdentityService/EntityFrameworks/EntityConfigurations/PermissionGrantedEntityTypeConfiguration.cs @@ -0,0 +1,21 @@ +using HelloShop.IdentityService.Entities; +using Microsoft.EntityFrameworkCore; + +namespace HelloShop.IdentityService.EntityFrameworks.EntityConfigurations; + +public class PermissionGrantedEntityTypeConfiguration : IEntityTypeConfiguration +{ + public void Configure(Microsoft.EntityFrameworkCore.Metadata.Builders.EntityTypeBuilder builder) + { + builder.ToTable("PermissionGranted"); + + builder.Property(x => x.Id); + builder.Property(x => x.PermissionName).HasMaxLength(64); + builder.Property(x => x.ResourceType).HasMaxLength(16); + builder.Property(x => x.ResourceId).HasMaxLength(32); + + builder.HasOne().WithMany().HasForeignKey(x => x.RoleId).IsRequired(); + + builder.HasIndex(x => new { x.RoleId, x.PermissionName, x.ResourceType, x.ResourceId }).IsUnique(); + } +} diff --git a/src/HelloShop.IdentityService/EntityFrameworks/Migrations/20240316084118_InitialCreate.Designer.cs b/src/HelloShop.IdentityService/EntityFrameworks/Migrations/20240327110650_InitialCreate.Designer.cs similarity index 87% rename from src/HelloShop.IdentityService/EntityFrameworks/Migrations/20240316084118_InitialCreate.Designer.cs rename to src/HelloShop.IdentityService/EntityFrameworks/Migrations/20240327110650_InitialCreate.Designer.cs index 835136d..f7b807a 100644 --- a/src/HelloShop.IdentityService/EntityFrameworks/Migrations/20240316084118_InitialCreate.Designer.cs +++ b/src/HelloShop.IdentityService/EntityFrameworks/Migrations/20240327110650_InitialCreate.Designer.cs @@ -12,7 +12,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; namespace HelloShop.IdentityService.EntityFrameworks.Migrations { [DbContext(typeof(IdentityServiceDbContext))] - [Migration("20240316084118_InitialCreate")] + [Migration("20240327110650_InitialCreate")] partial class InitialCreate { /// @@ -25,6 +25,38 @@ namespace HelloShop.IdentityService.EntityFrameworks.Migrations NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + modelBuilder.Entity("HelloShop.IdentityService.Entities.PermissionGranted", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("PermissionName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("ResourceId") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("ResourceType") + .HasMaxLength(16) + .HasColumnType("character varying(16)"); + + b.Property("RoleId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("RoleId", "PermissionName", "ResourceType", "ResourceId") + .IsUnique(); + + b.ToTable("PermissionGranted", (string)null); + }); + modelBuilder.Entity("HelloShop.IdentityService.Entities.Role", b => { b.Property("Id") @@ -262,6 +294,15 @@ namespace HelloShop.IdentityService.EntityFrameworks.Migrations b.ToTable("UserTokens", (string)null); }); + modelBuilder.Entity("HelloShop.IdentityService.Entities.PermissionGranted", b => + { + b.HasOne("HelloShop.IdentityService.Entities.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => { b.HasOne("HelloShop.IdentityService.Entities.Role", null) diff --git a/src/HelloShop.IdentityService/EntityFrameworks/Migrations/20240316084118_InitialCreate.cs b/src/HelloShop.IdentityService/EntityFrameworks/Migrations/20240327110650_InitialCreate.cs similarity index 86% rename from src/HelloShop.IdentityService/EntityFrameworks/Migrations/20240316084118_InitialCreate.cs rename to src/HelloShop.IdentityService/EntityFrameworks/Migrations/20240327110650_InitialCreate.cs index 2e55a16..68fb23e 100644 --- a/src/HelloShop.IdentityService/EntityFrameworks/Migrations/20240316084118_InitialCreate.cs +++ b/src/HelloShop.IdentityService/EntityFrameworks/Migrations/20240327110650_InitialCreate.cs @@ -55,6 +55,28 @@ namespace HelloShop.IdentityService.EntityFrameworks.Migrations table.PrimaryKey("PK_Users", x => x.Id); }); + migrationBuilder.CreateTable( + name: "PermissionGranted", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + RoleId = table.Column(type: "integer", nullable: false), + PermissionName = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), + ResourceType = table.Column(type: "character varying(16)", maxLength: 16, nullable: true), + ResourceId = table.Column(type: "character varying(32)", maxLength: 32, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_PermissionGranted", x => x.Id); + table.ForeignKey( + name: "FK_PermissionGranted_Roles_RoleId", + column: x => x.RoleId, + principalTable: "Roles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + migrationBuilder.CreateTable( name: "RoleClaims", columns: table => new @@ -161,6 +183,12 @@ namespace HelloShop.IdentityService.EntityFrameworks.Migrations onDelete: ReferentialAction.Cascade); }); + migrationBuilder.CreateIndex( + name: "IX_PermissionGranted_RoleId_PermissionName_ResourceType_Resour~", + table: "PermissionGranted", + columns: new[] { "RoleId", "PermissionName", "ResourceType", "ResourceId" }, + unique: true); + migrationBuilder.CreateIndex( name: "IX_RoleClaims_RoleId", table: "RoleClaims", @@ -202,6 +230,9 @@ namespace HelloShop.IdentityService.EntityFrameworks.Migrations /// protected override void Down(MigrationBuilder migrationBuilder) { + migrationBuilder.DropTable( + name: "PermissionGranted"); + migrationBuilder.DropTable( name: "RoleClaims"); diff --git a/src/HelloShop.IdentityService/EntityFrameworks/Migrations/IdentityServiceDbContextModelSnapshot.cs b/src/HelloShop.IdentityService/EntityFrameworks/Migrations/IdentityServiceDbContextModelSnapshot.cs index b7c373d..19ab4a6 100644 --- a/src/HelloShop.IdentityService/EntityFrameworks/Migrations/IdentityServiceDbContextModelSnapshot.cs +++ b/src/HelloShop.IdentityService/EntityFrameworks/Migrations/IdentityServiceDbContextModelSnapshot.cs @@ -22,6 +22,38 @@ namespace HelloShop.IdentityService.EntityFrameworks.Migrations NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + modelBuilder.Entity("HelloShop.IdentityService.Entities.PermissionGranted", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("PermissionName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("ResourceId") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("ResourceType") + .HasMaxLength(16) + .HasColumnType("character varying(16)"); + + b.Property("RoleId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("RoleId", "PermissionName", "ResourceType", "ResourceId") + .IsUnique(); + + b.ToTable("PermissionGranted", (string)null); + }); + modelBuilder.Entity("HelloShop.IdentityService.Entities.Role", b => { b.Property("Id") @@ -259,6 +291,15 @@ namespace HelloShop.IdentityService.EntityFrameworks.Migrations b.ToTable("UserTokens", (string)null); }); + modelBuilder.Entity("HelloShop.IdentityService.Entities.PermissionGranted", b => + { + b.HasOne("HelloShop.IdentityService.Entities.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => { b.HasOne("HelloShop.IdentityService.Entities.Role", null) diff --git a/src/HelloShop.IdentityService/Program.cs b/src/HelloShop.IdentityService/Program.cs index 28036e2..225d88c 100644 --- a/src/HelloShop.IdentityService/Program.cs +++ b/src/HelloShop.IdentityService/Program.cs @@ -1,8 +1,10 @@ using HelloShop.IdentityService; +using HelloShop.IdentityService.Authorization; using HelloShop.IdentityService.Constants; using HelloShop.IdentityService.DataSeeding; using HelloShop.IdentityService.Entities; using HelloShop.IdentityService.EntityFrameworks; +using HelloShop.ServiceDefaults.Authorization; using HelloShop.ServiceDefaults.Extensions; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.EntityFrameworkCore; @@ -54,6 +56,10 @@ 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"; +}); var app = builder.Build(); diff --git a/src/HelloShop.ServiceDefaults/Authorization/IPermissionChecker.cs b/src/HelloShop.ServiceDefaults/Authorization/IPermissionChecker.cs new file mode 100644 index 0000000..5726c2e --- /dev/null +++ b/src/HelloShop.ServiceDefaults/Authorization/IPermissionChecker.cs @@ -0,0 +1,10 @@ +using System.Security.Claims; + +namespace HelloShop.ServiceDefaults.Authorization; + +public interface IPermissionChecker +{ + Task IsGrantedAsync(string name, string? resourceType = null, string? resourceId = null); + + Task IsGrantedAsync(ClaimsPrincipal claimsPrincipal, string name, string? resourceType = null, string? resourceId = null); +} diff --git a/src/HelloShop.ServiceDefaults/Authorization/PermissionChecker.cs b/src/HelloShop.ServiceDefaults/Authorization/PermissionChecker.cs new file mode 100644 index 0000000..dc9fe9a --- /dev/null +++ b/src/HelloShop.ServiceDefaults/Authorization/PermissionChecker.cs @@ -0,0 +1,49 @@ +using HelloShop.ServiceDefaults.Constants; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Caching.Distributed; +using System.Security.Claims; + +namespace HelloShop.ServiceDefaults.Authorization; + +public abstract class PermissionChecker(IHttpContextAccessor httpContextAccessor, IDistributedCache distributedCache) : IPermissionChecker +{ + protected HttpContext HttpContext { get; init; } = httpContextAccessor.HttpContext ?? throw new InvalidOperationException(); + + public async Task IsGrantedAsync(string name, string? resourceType = null, string? resourceId = null) => await IsGrantedAsync(HttpContext.User, name, resourceType, resourceId); + + public async Task IsGrantedAsync(ClaimsPrincipal claimsPrincipal, string name, string? resourceType = null, string? resourceId = null) + { + var roleIds = claimsPrincipal.FindAll(CustomClaimTypes.RoleIdentifier).Select(c => Convert.ToInt32(c.Value)).ToArray(); + + foreach (var roleId in roleIds) + { + var cacheKey = PermissionGrantCacheItem.CreateCacheKey(roleId, name, resourceType, resourceId); + + if (distributedCache.TryGetValue(cacheKey, out PermissionGrantCacheItem? cacheItem) && cacheItem != null) + { + if (cacheItem.IsGranted) + { + return true; + } + + continue; + } + + bool isGranted = await IsGrantedAsync(roleId, name, resourceType, resourceId); + + await distributedCache.SetObjectAsync(cacheKey, new PermissionGrantCacheItem(isGranted), new DistributedCacheEntryOptions + { + SlidingExpiration = TimeSpan.FromMinutes(10) + }); + + if (isGranted) + { + return true; + } + } + + return false; + } + + public abstract Task IsGrantedAsync(int roleId, string name, string? resourceType = null, string? resourceId = null); +} diff --git a/src/HelloShop.ServiceDefaults/Authorization/PermissionGrantCacheItem.cs b/src/HelloShop.ServiceDefaults/Authorization/PermissionGrantCacheItem.cs new file mode 100644 index 0000000..36af03a --- /dev/null +++ b/src/HelloShop.ServiceDefaults/Authorization/PermissionGrantCacheItem.cs @@ -0,0 +1,23 @@ +namespace HelloShop.ServiceDefaults.Authorization; + +public class PermissionGrantCacheItem(bool isGranted = false) +{ + public bool IsGranted { get; set; } = isGranted; + + public static string CreateCacheKey(int roleId, string name, string? resourceType = null, string? resourceId = null) + { + string cacheKey = $"acl:role:{roleId}:{name}"; + + if (!string.IsNullOrWhiteSpace(resourceType)) + { + cacheKey += $":{resourceType}"; + } + + if (!string.IsNullOrWhiteSpace(resourceId)) + { + cacheKey += $":{resourceId}"; + } + + return cacheKey; + } +} diff --git a/src/HelloShop.ServiceDefaults/Authorization/PermissionGrantedResponse.cs b/src/HelloShop.ServiceDefaults/Authorization/PermissionGrantedResponse.cs new file mode 100644 index 0000000..d0e7318 --- /dev/null +++ b/src/HelloShop.ServiceDefaults/Authorization/PermissionGrantedResponse.cs @@ -0,0 +1,12 @@ +namespace HelloShop.ServiceDefaults.Authorization; + +public class PermissionGrantedResponse +{ + public required string Name { get; set; } + + public string? ResourceType { get; set; } + + public string? ResourceId { get; set; } + + public bool IsGranted { get; set; } +} diff --git a/src/HelloShop.ServiceDefaults/Authorization/RemotePermissionChecker.cs b/src/HelloShop.ServiceDefaults/Authorization/RemotePermissionChecker.cs new file mode 100644 index 0000000..aa40391 --- /dev/null +++ b/src/HelloShop.ServiceDefaults/Authorization/RemotePermissionChecker.cs @@ -0,0 +1,48 @@ + +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.WebUtilities; +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Options; +using System.Net.Http.Headers; +using System.Net.Http.Json; + +namespace HelloShop.ServiceDefaults.Authorization; + +public class RemotePermissionChecker(IHttpContextAccessor httpContextAccessor, IDistributedCache distributedCache, IHttpClientFactory httpClientFactory, IOptions options) : PermissionChecker(httpContextAccessor, distributedCache) +{ + private readonly RemotePermissionCheckerOptions _remotePermissionCheckerOptions = options.Value; + + public override async Task IsGrantedAsync(int roleId, string name, string? resourceType = null, string? resourceId = null) + { + string? accessToken = await HttpContext.GetTokenAsync("access_token"); + + HttpClient httpClient = httpClientFactory.CreateClient(); + + httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); + + httpClient.BaseAddress = new Uri(_remotePermissionCheckerOptions.ApiEndpoint); + + Dictionary parameters = new() + { + { nameof(roleId), roleId.ToString() }, + { nameof(name), name }, + { nameof(resourceType) , resourceType }, + { nameof(resourceId), resourceId } + }; + + string queryString = QueryHelpers.AddQueryString(string.Empty, parameters); + + var permissionGrants = httpClient.GetFromJsonAsAsyncEnumerable(queryString); + + await foreach (var permissionGrant in permissionGrants) + { + if (permissionGrant != null && permissionGrant.IsGranted) + { + return true; + } + } + + return false; + } +} diff --git a/src/HelloShop.ServiceDefaults/Authorization/RemotePermissionCheckerOptions.cs b/src/HelloShop.ServiceDefaults/Authorization/RemotePermissionCheckerOptions.cs new file mode 100644 index 0000000..dd38ad4 --- /dev/null +++ b/src/HelloShop.ServiceDefaults/Authorization/RemotePermissionCheckerOptions.cs @@ -0,0 +1,6 @@ +namespace HelloShop.ServiceDefaults.Authorization; + +public class RemotePermissionCheckerOptions +{ + public string ApiEndpoint { get; set; } = default!; +} diff --git a/src/HelloShop.ServiceDefaults/Extensions/CustomDistributedCacheExtensions.cs b/src/HelloShop.ServiceDefaults/Extensions/CustomDistributedCacheExtensions.cs new file mode 100644 index 0000000..b4a7b7a --- /dev/null +++ b/src/HelloShop.ServiceDefaults/Extensions/CustomDistributedCacheExtensions.cs @@ -0,0 +1,174 @@ +using Microsoft.Extensions.Caching.Distributed; +using System.Text.Json; + +namespace Microsoft.Extensions.Caching.Distributed; + +public static class CustomDistributedCacheExtensions +{ + /// + /// Sets a string in the specified cache with the specified key. + /// + /// The cache in which to store the data. + /// The key to store the data in. + /// The data to store in the cache. + /// Thrown when or is null. + public static void SetObject(this IDistributedCache cache, string key, TItem value) + { + var bytes = JsonSerializer.SerializeToUtf8Bytes(value); + cache.Set(key, bytes); + } + + /// + /// Sets a string in the specified cache with the specified key. + /// + /// The cache in which to store the data. + /// The key to store the data in. + /// The data to store in the cache. + /// The cache options for the entry. + /// Thrown when or is null. + public static void SetObject(this IDistributedCache cache, string key, TItem value, DistributedCacheEntryOptions options) + { + var bytes = JsonSerializer.SerializeToUtf8Bytes(value); + cache.Set(key, bytes, options); + } + + /// + /// Asynchronously sets a string in the specified cache with the specified key. + /// + /// The cache in which to store the data. + /// The key to store the data in. + /// The data to store in the cache. + /// Optional. A to cancel the operation. + /// A task that represents the asynchronous set operation. + /// Thrown when or is null. + public static Task SetObjectAsync(this IDistributedCache cache, string key, TItem value, CancellationToken token = default) + { + var bytes = JsonSerializer.SerializeToUtf8Bytes(value); + return cache.SetAsync(key, bytes, new DistributedCacheEntryOptions(), token); + } + + /// + /// Asynchronously sets a string in the specified cache with the specified key. + /// + /// The cache in which to store the data. + /// The key to store the data in. + /// The data to store in the cache. + /// The cache options for the entry. + /// Optional. A to cancel the operation. + /// A task that represents the asynchronous set operation. + /// Thrown when or is null. + public static Task SetObjectAsync(this IDistributedCache cache, string key, TItem value, DistributedCacheEntryOptions options, CancellationToken token = default) + { + var bytes = JsonSerializer.SerializeToUtf8Bytes(value); + return cache.SetAsync(key, bytes, options, token); + } + + /// + /// Gets a string from the specified cache with the specified key. + /// + /// The cache in which to store the data. + /// The key to get the stored data for. + /// The T value from the stored cache key. + public static TItem? GetObject(this IDistributedCache cache, string key) + { + byte[]? data = cache.Get(key); + + if (data == null) + { + return default; + } + + return JsonSerializer.Deserialize(data); + } + + /// + /// Asynchronously gets a string from the specified cache with the specified key. + /// + /// The cache in which to store the data. + /// The key to get the stored data for. + /// Optional. A to cancel the operation. + /// A task that gets the T value from the stored cache key. + public static async Task GetObjectAsync(this IDistributedCache cache, string key, CancellationToken token = default) + { + byte[]? data = await cache.GetAsync(key, token).ConfigureAwait(false); + + if (data == null) + { + return default; + } + + return JsonSerializer.Deserialize(data); + } + + /// + /// Try to get the value associated with the given key. + /// + /// The type of the object to get. + /// The instance this method extends. + /// The key of the value to get. + /// The value associated with the given key. + /// true if the key was found. false otherwise. + public static bool TryGetValue(this IDistributedCache cache, string key, out TItem? value) + { + var data = cache.Get(key); + + value = default; + + try + { + value = JsonSerializer.Deserialize(data); + } + catch + { + return false; + } + + return true; + } + + /// + /// Gets the value associated with this key if it exists, or generates a new entry using the provided key and a value from the given factory if the key is not found. + /// + /// The type of the object to get. + /// The instance this method extends. + /// The key of the entry to look for or create. + /// The factory that creates the value associated with this key if the key does not exist in the cache. + /// The options to be applied to the if the key does not exist in the cache. + /// The value associated with this key. + public static TItem? GetOrCreate(this IDistributedCache cache, string key, Func factory, DistributedCacheEntryOptions? createOptions = null) + { + if (!cache.TryGetValue(key, out object? result)) + { + createOptions ??= new DistributedCacheEntryOptions(); + + result = factory(createOptions); + + cache.SetObject(key, result, createOptions); + } + + return (TItem?)result; + } + + /// + /// Asynchronously gets the value associated with this key if it exists, or generates a new entry using the provided key and a value from the given factory if the key is not found. + /// + /// The type of the object to get. + /// The instance this method extends. + /// The key of the entry to look for or create. + /// The factory task that creates the value associated with this key if the key does not exist in the cache. + /// The options to be applied to the if the key does not exist in the cache. + /// The task object representing the asynchronous operation. + public static async Task GetOrCreateAsync(this IDistributedCache cache, string key, Func> factory, DistributedCacheEntryOptions? createOptions = null) + { + if (!cache.TryGetValue(key, out object? result)) + { + createOptions ??= new DistributedCacheEntryOptions(); + + result = await factory(createOptions).ConfigureAwait(false); + + await cache.SetObjectAsync(key, result, createOptions); + } + + return (TItem?)result; + } +} diff --git a/src/HelloShop.ServiceDefaults/Extensions/PermissionExtensions.cs b/src/HelloShop.ServiceDefaults/Extensions/PermissionExtensions.cs index 06fbd17..0d68f8b 100644 --- a/src/HelloShop.ServiceDefaults/Extensions/PermissionExtensions.cs +++ b/src/HelloShop.ServiceDefaults/Extensions/PermissionExtensions.cs @@ -1,4 +1,5 @@ -using HelloShop.ServiceDefaults.Permissions; +using HelloShop.ServiceDefaults.Authorization; +using HelloShop.ServiceDefaults.Permissions; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; @@ -21,7 +22,7 @@ public static class PermissionExtensions return services; } - + public static IEndpointRouteBuilder MapPermissionDefinitions(this IEndpointRouteBuilder endpoints, params string[] tags) { var routeGroup = endpoints.MapGroup(string.Empty); @@ -62,4 +63,13 @@ public static class PermissionExtensions return routeGroup; } + + public static IServiceCollection AddRemotePermissionChecker(this IServiceCollection services, Action configureOptions) + { + services.Configure(configureOptions); + + services.AddTransient(); + + return services; + } }