diff --git a/HelloShop.sln b/HelloShop.sln index ac47d29..3c1be0d 100644 --- a/HelloShop.sln +++ b/HelloShop.sln @@ -1,4 +1,4 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 +Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.9.34414.90 MinimumVisualStudioVersion = 10.0.40219.1 @@ -18,8 +18,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HelloShop.ProductService", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HelloShop.BasketService", "src\HelloShop.BasketService\HelloShop.BasketService.csproj", "{02EBA5AD-84B4-4AF4-B519-72061C08800D}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HelloShop.HybridApp", "src\HelloShop.HybridApp\HelloShop.HybridApp.csproj", "{CC0E5839-B7E9-400E-9AF3-95863BBF518B}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{1AD03316-A743-4E9D-B3BC-FB9499D15141}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{29BE158E-825E-48AB-A02D-4E537A5DC502}" @@ -66,12 +64,6 @@ Global {02EBA5AD-84B4-4AF4-B519-72061C08800D}.Debug|Any CPU.Build.0 = Debug|Any CPU {02EBA5AD-84B4-4AF4-B519-72061C08800D}.Release|Any CPU.ActiveCfg = Release|Any CPU {02EBA5AD-84B4-4AF4-B519-72061C08800D}.Release|Any CPU.Build.0 = Release|Any CPU - {CC0E5839-B7E9-400E-9AF3-95863BBF518B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {CC0E5839-B7E9-400E-9AF3-95863BBF518B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {CC0E5839-B7E9-400E-9AF3-95863BBF518B}.Debug|Any CPU.Deploy.0 = Debug|Any CPU - {CC0E5839-B7E9-400E-9AF3-95863BBF518B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {CC0E5839-B7E9-400E-9AF3-95863BBF518B}.Release|Any CPU.Build.0 = Release|Any CPU - {CC0E5839-B7E9-400E-9AF3-95863BBF518B}.Release|Any CPU.Deploy.0 = Release|Any CPU {2022279A-E39F-4489-82AE-39AC53C594C9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {2022279A-E39F-4489-82AE-39AC53C594C9}.Debug|Any CPU.Build.0 = Debug|Any CPU {2022279A-E39F-4489-82AE-39AC53C594C9}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -93,7 +85,6 @@ Global {C4789097-3694-4370-9252-44268661A26E} = {1AD03316-A743-4E9D-B3BC-FB9499D15141} {B4C0ADA2-0442-4B7E-BFD9-AA39B52A0D42} = {1AD03316-A743-4E9D-B3BC-FB9499D15141} {02EBA5AD-84B4-4AF4-B519-72061C08800D} = {1AD03316-A743-4E9D-B3BC-FB9499D15141} - {CC0E5839-B7E9-400E-9AF3-95863BBF518B} = {1AD03316-A743-4E9D-B3BC-FB9499D15141} {2022279A-E39F-4489-82AE-39AC53C594C9} = {29BE158E-825E-48AB-A02D-4E537A5DC502} {45932B7F-6ED0-40F3-AA2C-F14A844FEE18} = {29BE158E-825E-48AB-A02D-4E537A5DC502} EndGlobalSection diff --git a/src/HelloShop.IdentityService/Controllers/TestsController.cs b/src/HelloShop.IdentityService/Controllers/TestsController.cs index baf5a53..d1cfe20 100644 --- a/src/HelloShop.IdentityService/Controllers/TestsController.cs +++ b/src/HelloShop.IdentityService/Controllers/TestsController.cs @@ -12,19 +12,20 @@ namespace HelloShop.IdentityService.Controllers public class TestsController : ControllerBase { [HttpGet(nameof(Foo))] - [Authorize(Roles = "AdminRole")] + [Authorize(IdentityPermissions.Users.Update)] public IActionResult Foo() { return Ok("Hello, World!"); } [HttpGet(nameof(Bar))] - [Authorize(Roles = "GuestRole")] + [Authorize(IdentityPermissions.Users.Create)] public IActionResult Bar() { return Ok("Hello, World!"); } + [Authorize(IdentityPermissions.Users.Delete)] [HttpGet(nameof(Baz))] public IActionResult Baz() { diff --git a/src/HelloShop.IdentityService/PermissionProviders/IdentityPermissionDefinitionProvider.cs b/src/HelloShop.IdentityService/PermissionProviders/IdentityPermissionDefinitionProvider.cs new file mode 100644 index 0000000..ff6f209 --- /dev/null +++ b/src/HelloShop.IdentityService/PermissionProviders/IdentityPermissionDefinitionProvider.cs @@ -0,0 +1,25 @@ +using HelloShop.ServiceDefaults.Permissions; + +namespace HelloShop.IdentityService; + +public class IdentityPermissionDefinitionProvider : IPermissionDefinitionProvider +{ + public void Define(PermissionDefinitionContext context) + { + var identityGroup = context.AddGroup(IdentityPermissions.GroupName, "访问控制"); + + var roles = identityGroup.AddPermission(IdentityPermissions.Roles.Default, "角色管理"); + + roles.AddChild(IdentityPermissions.Roles.Create, "创建角色"); + roles.AddChild(IdentityPermissions.Roles.Update, "更新角色"); + roles.AddChild(IdentityPermissions.Roles.Delete, "删除角色"); + roles.AddChild(IdentityPermissions.Roles.ManagePermissions, "管理角色权限"); + + var users = identityGroup.AddPermission(IdentityPermissions.Users.Default, "用户管理"); + + users.AddChild(IdentityPermissions.Users.Create, "创建用户"); + users.AddChild(IdentityPermissions.Users.Update, "更新用户"); + users.AddChild(IdentityPermissions.Users.Delete, "删除用户"); + users.AddChild(IdentityPermissions.Users.ManageRoles, "管理用户角色"); + } +} diff --git a/src/HelloShop.IdentityService/PermissionProviders/IdentityPermissions.cs b/src/HelloShop.IdentityService/PermissionProviders/IdentityPermissions.cs new file mode 100644 index 0000000..efc55e1 --- /dev/null +++ b/src/HelloShop.IdentityService/PermissionProviders/IdentityPermissions.cs @@ -0,0 +1,24 @@ +namespace HelloShop.IdentityService; + +public static class IdentityPermissions +{ + public const string GroupName = "Identity"; + + public static class Roles + { + public const string Default = GroupName + ".Roles"; + public const string Create = Default + ".Create"; + public const string Update = Default + ".Update"; + public const string Delete = Default + ".Delete"; + public const string ManagePermissions = Default + ".ManagePermissions"; + } + + public static class Users + { + public const string Default = GroupName + ".Users"; + public const string Create = Default + ".Create"; + public const string Update = Default + ".Update"; + public const string Delete = Default + ".Delete"; + public const string ManageRoles = Update + ".ManageRoles"; + } +} diff --git a/src/HelloShop.IdentityService/Program.cs b/src/HelloShop.IdentityService/Program.cs index 3024467..28036e2 100644 --- a/src/HelloShop.IdentityService/Program.cs +++ b/src/HelloShop.IdentityService/Program.cs @@ -53,6 +53,7 @@ builder.Services.AddAuthentication(options => builder.Services.AddDataSeedingProviders(); builder.Services.AddOpenApi(); +builder.Services.AddPermissionDefinitions(); var app = builder.Build(); @@ -66,5 +67,6 @@ app.MapControllers(); app.UseDataSeedingProviders(); app.UseOpenApi(); +app.MapGroup("api/Permissions").MapPermissionDefinitions("Permissions"); app.Run(); diff --git a/src/HelloShop.ServiceDefaults/Extensions/PermissionExtensions.cs b/src/HelloShop.ServiceDefaults/Extensions/PermissionExtensions.cs new file mode 100644 index 0000000..06fbd17 --- /dev/null +++ b/src/HelloShop.ServiceDefaults/Extensions/PermissionExtensions.cs @@ -0,0 +1,65 @@ +using HelloShop.ServiceDefaults.Permissions; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; +using System.Reflection; + +namespace HelloShop.ServiceDefaults.Extensions; + +public static class PermissionExtensions +{ + public static IServiceCollection AddPermissionDefinitions(this IServiceCollection services, Assembly? assembly = null) + { + assembly ??= Assembly.GetCallingAssembly(); + + var permissionDefinitionProviders = assembly.ExportedTypes.Where(t => t.IsAssignableTo(typeof(IPermissionDefinitionProvider))); + + permissionDefinitionProviders.ToList().ForEach(t => services.AddSingleton(typeof(IPermissionDefinitionProvider), t)); + + services.AddSingleton(); + + return services; + } + + public static IEndpointRouteBuilder MapPermissionDefinitions(this IEndpointRouteBuilder endpoints, params string[] tags) + { + var routeGroup = endpoints.MapGroup(string.Empty); + + routeGroup.MapGet("PermissionDefinitions", async (IPermissionDefinitionManager permissionDefinitionManager) => + { + List result = []; + + var permissionGroups = await permissionDefinitionManager.GetGroupsAsync(); + + foreach (var permissionGroup in permissionGroups) + { + PermissionGroupDefinitionResponse permissionGroupDefinition = new() + { + Name = permissionGroup.Name, + DisplayName = permissionGroup.DisplayName, + Permissions = [] + }; + + foreach (PermissionDefinition? permission in permissionGroup.GetPermissionsWithChildren()) + { + PermissionDefinitionResponse permissionDefinition = new() + { + Name = permission.Name, + DisplayName = permission.DisplayName, + ParentName = permission.Parent?.Name + }; + + permissionGroupDefinition.Permissions.Add(permissionDefinition); + } + + result.Add(permissionGroupDefinition); + } + + return result; + + }).WithTags(tags); + + return routeGroup; + } +} diff --git a/src/HelloShop.ServiceDefaults/Models/Permissions/PermissionDefinitionResponse.cs b/src/HelloShop.ServiceDefaults/Models/Permissions/PermissionDefinitionResponse.cs new file mode 100644 index 0000000..670b569 --- /dev/null +++ b/src/HelloShop.ServiceDefaults/Models/Permissions/PermissionDefinitionResponse.cs @@ -0,0 +1,10 @@ +namespace HelloShop.ServiceDefaults.Permissions; + +public class PermissionDefinitionResponse +{ + public required string Name { get; init; } + + public string? DisplayName { get; set; } + + public string? ParentName { get; set; } +} diff --git a/src/HelloShop.ServiceDefaults/Models/Permissions/PermissionGroupDefinitionResponse.cs b/src/HelloShop.ServiceDefaults/Models/Permissions/PermissionGroupDefinitionResponse.cs new file mode 100644 index 0000000..6387b9f --- /dev/null +++ b/src/HelloShop.ServiceDefaults/Models/Permissions/PermissionGroupDefinitionResponse.cs @@ -0,0 +1,10 @@ +namespace HelloShop.ServiceDefaults.Permissions; + +public class PermissionGroupDefinitionResponse +{ + public required string Name { get; init; } + + public string? DisplayName { get; set; } + + public required List Permissions { get; init; } +} diff --git a/src/HelloShop.ServiceDefaults/Permissions/IPermissionDefinitionManager.cs b/src/HelloShop.ServiceDefaults/Permissions/IPermissionDefinitionManager.cs new file mode 100644 index 0000000..90e570d --- /dev/null +++ b/src/HelloShop.ServiceDefaults/Permissions/IPermissionDefinitionManager.cs @@ -0,0 +1,12 @@ +namespace HelloShop.ServiceDefaults.Permissions; + +public interface IPermissionDefinitionManager +{ + Task GetAsync(string name); + + Task GetOrNullAsync(string name); + + Task> GetPermissionsAsync(); + + Task> GetGroupsAsync(); +} diff --git a/src/HelloShop.ServiceDefaults/Permissions/IPermissionDefinitionProvider.cs b/src/HelloShop.ServiceDefaults/Permissions/IPermissionDefinitionProvider.cs new file mode 100644 index 0000000..335a846 --- /dev/null +++ b/src/HelloShop.ServiceDefaults/Permissions/IPermissionDefinitionProvider.cs @@ -0,0 +1,6 @@ +namespace HelloShop.ServiceDefaults.Permissions; + +public interface IPermissionDefinitionProvider +{ + void Define(PermissionDefinitionContext context); +} diff --git a/src/HelloShop.ServiceDefaults/Permissions/PermissionDefinition.cs b/src/HelloShop.ServiceDefaults/Permissions/PermissionDefinition.cs new file mode 100644 index 0000000..8a0c682 --- /dev/null +++ b/src/HelloShop.ServiceDefaults/Permissions/PermissionDefinition.cs @@ -0,0 +1,30 @@ +namespace HelloShop.ServiceDefaults.Permissions; + +public class PermissionDefinition +{ + public string Name { get; } = default!; + + public string? DisplayName { get; set; } + + public PermissionDefinition? Parent { get; private set; } + + public bool IsEnabled { get; set; } + + private readonly List _children = []; + + public IReadOnlyList Children => [.. _children]; + + protected internal PermissionDefinition(string name, string? displayName = null, bool isEnabled = true) + { + Name = name; + DisplayName = displayName; + IsEnabled = isEnabled; + } + + public virtual PermissionDefinition AddChild(string name, string? displayName = null, bool isEnabled = true) + { + var child = new PermissionDefinition(name, displayName, isEnabled) { Parent = this }; + _children.Add(child); + return child; + } +} diff --git a/src/HelloShop.ServiceDefaults/Permissions/PermissionDefinitionContext.cs b/src/HelloShop.ServiceDefaults/Permissions/PermissionDefinitionContext.cs new file mode 100644 index 0000000..6af2fab --- /dev/null +++ b/src/HelloShop.ServiceDefaults/Permissions/PermissionDefinitionContext.cs @@ -0,0 +1,66 @@ +namespace HelloShop.ServiceDefaults.Permissions; + +public class PermissionDefinitionContext +{ + public IServiceProvider ServiceProvider { get; } + + internal Dictionary Groups { get; } + + internal PermissionDefinitionContext(IServiceProvider serviceProvider) + { + ServiceProvider = serviceProvider; + Groups = []; + } + + public virtual PermissionGroupDefinition AddGroup(string name, string? displayName = null) + { + if (Groups.ContainsKey(name)) + { + throw new InvalidOperationException($"There is already an existing permission group with name: {name}"); + } + + return Groups[name] = new PermissionGroupDefinition(name, displayName); + } + + public virtual PermissionGroupDefinition GetGroup(string name) + { + PermissionGroupDefinition? group = GetGroupOrNull(name); + + return group is null ? throw new InvalidOperationException($"Could not find a permission definition group with the given name: {name}") : group; + } + + public virtual PermissionGroupDefinition? GetGroupOrNull(string name) + { + if (!Groups.TryGetValue(name, out PermissionGroupDefinition? value)) + { + return null; + } + + return value; + } + + public virtual void RemoveGroup(string name) + { + if (!Groups.ContainsKey(name)) + { + throw new InvalidOperationException($"Not found permission group with name: {name}"); + } + + Groups.Remove(name); + } + + public virtual PermissionDefinition? GetPermissionOrNull(string name) + { + foreach (var groupDefinition in Groups.Values) + { + var permissionDefinition = groupDefinition.GetPermissionOrNull(name); + + if (permissionDefinition != null) + { + return permissionDefinition; + } + } + + return null; + } +} diff --git a/src/HelloShop.ServiceDefaults/Permissions/PermissionDefinitionManager.cs b/src/HelloShop.ServiceDefaults/Permissions/PermissionDefinitionManager.cs new file mode 100644 index 0000000..03133d8 --- /dev/null +++ b/src/HelloShop.ServiceDefaults/Permissions/PermissionDefinitionManager.cs @@ -0,0 +1,96 @@ + +using Microsoft.Extensions.DependencyInjection; + +namespace HelloShop.ServiceDefaults.Permissions; + +public class PermissionDefinitionManager : IPermissionDefinitionManager +{ + private readonly Lazy> _lazyPermissionGroupDefinitions; + + protected IDictionary PermissionGroupDefinitions => _lazyPermissionGroupDefinitions.Value; + + private readonly Lazy> _lazyPermissionDefinitions; + + protected IDictionary PermissionDefinitions => _lazyPermissionDefinitions.Value; + + private readonly IServiceProvider _serviceProvider; + + public PermissionDefinitionManager(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + _lazyPermissionGroupDefinitions = new Lazy>(CreatePermissionGroupDefinitions, isThreadSafe: true); + _lazyPermissionDefinitions = new Lazy>(CreatePermissionDefinitions, isThreadSafe: true); + } + + protected virtual Dictionary CreatePermissionGroupDefinitions() + { + using var scope = _serviceProvider.CreateScope(); + + var context = new PermissionDefinitionContext(scope.ServiceProvider); + + var providers = _serviceProvider.GetServices(); + + foreach (IPermissionDefinitionProvider provider in providers) + { + provider.Define(context); + } + + return context.Groups; + } + + protected virtual Dictionary CreatePermissionDefinitions() + { + var permissions = new Dictionary(); + + foreach (var groupDefinition in PermissionGroupDefinitions.Values) + { + foreach (var permission in groupDefinition.Permissions) + { + AddPermissionToDictionaryRecursively(permissions, permission); + } + } + + return permissions; + } + + protected virtual void AddPermissionToDictionaryRecursively(Dictionary permissions, PermissionDefinition permission) + { + if (permissions.ContainsKey(permission.Name)) + { + throw new InvalidOperationException($"Duplicate permission name {permission.Name}"); + } + + permissions[permission.Name] = permission; + + foreach (var child in permission.Children) + { + AddPermissionToDictionaryRecursively(permissions, child); + } + } + + public async Task GetAsync(string name) + { + var permission = await GetOrNullAsync(name); + + return permission ?? throw new InvalidOperationException($"Undefined permission {name}"); + } + + public Task GetOrNullAsync(string name) + { + return Task.FromResult(PermissionDefinitions.TryGetValue(name, out var obj) ? obj : default); + } + + public Task> GetGroupsAsync() + { + IReadOnlyList permissionGroups = [.. PermissionGroupDefinitions.Values]; + + return Task.FromResult(permissionGroups); + } + + public Task> GetPermissionsAsync() + { + IReadOnlyList permissions = [.. PermissionDefinitions.Values]; + + return Task.FromResult(permissions); + } +} diff --git a/src/HelloShop.ServiceDefaults/Permissions/PermissionGroupDefinition.cs b/src/HelloShop.ServiceDefaults/Permissions/PermissionGroupDefinition.cs new file mode 100644 index 0000000..047471b --- /dev/null +++ b/src/HelloShop.ServiceDefaults/Permissions/PermissionGroupDefinition.cs @@ -0,0 +1,68 @@ +namespace HelloShop.ServiceDefaults.Permissions; + +public class PermissionGroupDefinition +{ + public string Name { get; } = default!; + + public string? DisplayName { get; set; } + + private readonly List _permissions = []; + + public IReadOnlyList Permissions => [.. _permissions]; + + protected internal PermissionGroupDefinition(string name, string? displayName = null) + { + Name = name; + DisplayName = displayName; + } + + public virtual PermissionDefinition AddPermission(string name, string? displayName = null, bool isEnabled = true) + { + var permission = new PermissionDefinition(name, displayName, isEnabled); + _permissions.Add(permission); + return permission; + } + + public virtual List GetPermissionsWithChildren() + { + var permissions = new List(); + + foreach (var permission in _permissions) + { + AddPermissionToListRecursively(permissions, permission); + } + + return permissions; + } + + private static void AddPermissionToListRecursively(List permissions, PermissionDefinition permission) + { + permissions.Add(permission); + + foreach (var child in permission.Children) + { + AddPermissionToListRecursively(permissions, child); + } + } + + public PermissionDefinition? GetPermissionOrNull(string name) => GetPermissionOrNullRecursively(Permissions, name); + + private static PermissionDefinition? GetPermissionOrNullRecursively(IReadOnlyList permissions, string name) + { + foreach (var permission in permissions) + { + if (permission.Name == name) + { + return permission; + } + + var childPermission = GetPermissionOrNullRecursively(permission.Children, name); + if (childPermission != null) + { + return childPermission; + } + } + + return null; + } +}