From 2600b6bf0808ba212615b5a25ab7ed228113bf6d Mon Sep 17 00:00:00 2001 From: Galip Tolga Erdem Date: Wed, 26 Jul 2023 20:24:42 -0400 Subject: [PATCH] Added caching --- .env.example | 2 - .../Roles/KeycloakRoleCreationJob.cs | 6 +- .../Roles/KeycloakRoleDeletionJob.cs | 6 +- .../Roles/KeycloakRoleUpdatingJob.cs | 28 ++- .../Users/KeycloakUserCreationJob.cs | 24 ++- .../Users/KeycloakUserDeletionJob.cs | 11 +- .../Users/KeycloakUserUpdatingJob.cs | 95 +++++++-- ...opOnAbpIdentityServiceAutoMapperProfile.cs | 21 ++ .../Roles/KeycloakRolesEventHandler.cs | 47 +++++ .../Identity/EShopIdentityRoleAppService.cs | 7 +- .../Identity/EShopIdentityUserAppService.cs | 47 +++-- .../IdentityServiceApplicationModule.cs | 1 + .../Keycloak/KeycloakService.cs | 85 -------- .../Keycloak/Service/CachedKeycloakRole.cs | 23 +++ .../Keycloak/Service/CachedKeycloakService.cs | 127 ++++++++++++ .../Keycloak/Service/CachedKeycloakUser.cs | 75 ++++++++ .../Keycloak/Service/IKeycloakService.cs | 37 ++++ .../{ => Service}/KeycloakClientOptions.cs | 2 +- .../Keycloak/Service/KeycloakService.cs | 182 ++++++++++++++++++ .../Service/KeycloakServiceExtensions.cs | 31 +++ 20 files changed, 708 insertions(+), 149 deletions(-) delete mode 100644 .env.example create mode 100644 services/identity/src/EShopOnAbp.IdentityService.Application/EShopOnAbpIdentityServiceAutoMapperProfile.cs create mode 100644 services/identity/src/EShopOnAbp.IdentityService.Application/EventHandlers/Roles/KeycloakRolesEventHandler.cs delete mode 100644 services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/KeycloakService.cs create mode 100644 services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/Service/CachedKeycloakRole.cs create mode 100644 services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/Service/CachedKeycloakService.cs create mode 100644 services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/Service/CachedKeycloakUser.cs create mode 100644 services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/Service/IKeycloakService.cs rename services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/{ => Service}/KeycloakClientOptions.cs (78%) create mode 100644 services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/Service/KeycloakService.cs create mode 100644 services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/Service/KeycloakServiceExtensions.cs diff --git a/.env.example b/.env.example deleted file mode 100644 index 4f042dca..00000000 --- a/.env.example +++ /dev/null @@ -1,2 +0,0 @@ -#Payment__PayPal__ClientId=PAYPAL_CLIENT_ID -#Payment__PayPal__Secret=PAYPAL_SECRET diff --git a/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/Roles/KeycloakRoleCreationJob.cs b/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/Roles/KeycloakRoleCreationJob.cs index 096b0c61..58bdacb3 100644 --- a/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/Roles/KeycloakRoleCreationJob.cs +++ b/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/Roles/KeycloakRoleCreationJob.cs @@ -1,7 +1,7 @@ using System; using System.Linq; using System.Threading.Tasks; -using EShopOnAbp.IdentityService.Keycloak; +using EShopOnAbp.IdentityService.Keycloak.Service; using Microsoft.Extensions.Logging; using Volo.Abp.BackgroundJobs; using Volo.Abp.DependencyInjection; @@ -10,10 +10,10 @@ namespace EShopOnAbp.IdentityService.BackgroundJobs.Roles; public class KeycloakRoleCreationJob : AsyncBackgroundJob, ITransientDependency { - private readonly KeycloakService _keycloakService; + private readonly IKeycloakService _keycloakService; private readonly ILogger _logger; - public KeycloakRoleCreationJob(KeycloakService keycloakService, ILogger logger) + public KeycloakRoleCreationJob(IKeycloakService keycloakService, ILogger logger) { _keycloakService = keycloakService; _logger = logger; diff --git a/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/Roles/KeycloakRoleDeletionJob.cs b/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/Roles/KeycloakRoleDeletionJob.cs index 812bf290..cba36100 100644 --- a/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/Roles/KeycloakRoleDeletionJob.cs +++ b/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/Roles/KeycloakRoleDeletionJob.cs @@ -1,7 +1,7 @@ using System; using System.Linq; using System.Threading.Tasks; -using EShopOnAbp.IdentityService.Keycloak; +using EShopOnAbp.IdentityService.Keycloak.Service; using Microsoft.Extensions.Logging; using Volo.Abp.BackgroundJobs; using Volo.Abp.DependencyInjection; @@ -10,10 +10,10 @@ namespace EShopOnAbp.IdentityService.BackgroundJobs.Roles; public class KeycloakRoleDeletionJob : AsyncBackgroundJob, ITransientDependency { - private readonly KeycloakService _keycloakService; + private readonly IKeycloakService _keycloakService; private readonly ILogger _logger; - public KeycloakRoleDeletionJob(KeycloakService keycloakService, ILogger logger) + public KeycloakRoleDeletionJob(IKeycloakService keycloakService, ILogger logger) { _keycloakService = keycloakService; _logger = logger; diff --git a/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/Roles/KeycloakRoleUpdatingJob.cs b/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/Roles/KeycloakRoleUpdatingJob.cs index b7d0f95c..3d4050a9 100644 --- a/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/Roles/KeycloakRoleUpdatingJob.cs +++ b/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/Roles/KeycloakRoleUpdatingJob.cs @@ -1,45 +1,55 @@ using System; using System.Linq; using System.Threading.Tasks; -using EShopOnAbp.IdentityService.Keycloak; +using EShopOnAbp.IdentityService.Keycloak.Service; +using Keycloak.Net.Models.Roles; using Microsoft.Extensions.Logging; using Volo.Abp.BackgroundJobs; using Volo.Abp.DependencyInjection; +using Volo.Abp.ObjectMapping; namespace EShopOnAbp.IdentityService.BackgroundJobs.Roles; public class KeycloakRoleUpdatingJob : AsyncBackgroundJob, ITransientDependency { - private readonly KeycloakService _keycloakService; + private readonly IKeycloakService _keycloakService; private readonly ILogger _logger; + private readonly IObjectMapper _objectMapper; - public KeycloakRoleUpdatingJob(KeycloakService keycloakService, ILogger logger) + public KeycloakRoleUpdatingJob(IKeycloakService keycloakService, ILogger logger, + IObjectMapper objectMapper) { _keycloakService = keycloakService; _logger = logger; + _objectMapper = objectMapper; } public override async Task ExecuteAsync(IdentityRoleUpdatingArgs args) { try { - var existingRole = (await _keycloakService.GetRolesAsync()).FirstOrDefault(q => q.Name == args.Name); + var existingRole = (await _keycloakService.GetRolesAsync()).FirstOrDefault(q => q.Name == args.OldName); if (existingRole == null) { - _logger.LogWarning($"Role with the name:{args.Name} couldn't be found to update!"); + _logger.LogWarning($"Role with the name:{args.OldName} couldn't be found to update!"); return; } - existingRole.Name = args.Name; + if (args.OldName != args.NewName) + { + existingRole.Name = args.NewName; - await _keycloakService.UpdateRoleAsync(existingRole.Id, existingRole); + await _keycloakService.UpdateRoleAsync(existingRole.Id, + _objectMapper.Map(existingRole) + ); + } } catch (Exception e) { - _logger.LogWarning($"Could not delete the role with the name:{args.Name} from Keycloak server!"); + _logger.LogWarning($"Could not update the role with the name:{args.OldName} from Keycloak server!"); throw; } } } -public record IdentityRoleUpdatingArgs(string Name); \ No newline at end of file +public record IdentityRoleUpdatingArgs(string OldName, string NewName); \ No newline at end of file diff --git a/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/Users/KeycloakUserCreationJob.cs b/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/Users/KeycloakUserCreationJob.cs index 44f2e207..7f3b4400 100644 --- a/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/Users/KeycloakUserCreationJob.cs +++ b/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/Users/KeycloakUserCreationJob.cs @@ -2,24 +2,28 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using EShopOnAbp.IdentityService.Keycloak; +using EShopOnAbp.IdentityService.Keycloak.Service; +using Keycloak.Net.Models.Roles; using Keycloak.Net.Models.Users; using Microsoft.Extensions.Logging; using Volo.Abp.BackgroundJobs; using Volo.Abp.DependencyInjection; using Volo.Abp.Identity; +using Volo.Abp.ObjectMapping; namespace EShopOnAbp.IdentityService.BackgroundJobs.Users; public class KeycloakUserCreationJob : AsyncBackgroundJob, ITransientDependency { - private readonly KeycloakService _keycloakService; + private readonly IKeycloakService _keycloakService; private readonly ILogger _logger; + private readonly IObjectMapper _objectMapper; - public KeycloakUserCreationJob(KeycloakService keycloakService, - ILogger logger) + public KeycloakUserCreationJob(IKeycloakService keycloakService, + ILogger logger, IObjectMapper objectMapper) { _logger = logger; + _objectMapper = objectMapper; _keycloakService = keycloakService; } @@ -32,7 +36,6 @@ public class KeycloakUserCreationJob : AsyncBackgroundJob() { new() { Type = "password", Value = args.Password } @@ -61,11 +64,14 @@ public class KeycloakUserCreationJob : AsyncBackgroundJobq.UserName == userName); var allTheRoles = await _keycloakService.GetRolesAsync(); - var roles = allTheRoles.Where(q => roleNames.Contains(q.Name)); + var roles = allTheRoles.Where(q => roleNames.Contains(q.Name)).ToList(); - await _keycloakService.AddRolesToUserAsync(user.Id, roles); + await _keycloakService.AddRealmRolesToUserAsync( + user.Id, + _objectMapper.Map,List>(roles) + ); _logger.LogInformation($"Keycloak roles:{roleNames} has been added to user with the username:{userName}."); } } @@ -80,7 +86,7 @@ public class IdentityUserCreationArgs public bool IsActive { get; init; } public string[] RoleNames { get; init; } - public IdentityUserCreationArgs() + public IdentityUserCreationArgs() // For deserialization { } diff --git a/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/Users/KeycloakUserDeletionJob.cs b/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/Users/KeycloakUserDeletionJob.cs index 0dbdcb40..1c7d7c57 100644 --- a/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/Users/KeycloakUserDeletionJob.cs +++ b/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/Users/KeycloakUserDeletionJob.cs @@ -1,7 +1,7 @@ using System; using System.Linq; using System.Threading.Tasks; -using EShopOnAbp.IdentityService.Keycloak; +using EShopOnAbp.IdentityService.Keycloak.Service; using Microsoft.Extensions.Logging; using Volo.Abp; using Volo.Abp.BackgroundJobs; @@ -11,10 +11,10 @@ namespace EShopOnAbp.IdentityService.BackgroundJobs.Users; public class KeycloakUserDeletionJob : AsyncBackgroundJob, ITransientDependency { - private readonly KeycloakService _keycloakService; + private readonly IKeycloakService _keycloakService; private readonly ILogger _logger; - public KeycloakUserDeletionJob(KeycloakService keycloakService, + public KeycloakUserDeletionJob(IKeycloakService keycloakService, ILogger logger) { _keycloakService = keycloakService; @@ -25,8 +25,8 @@ public class KeycloakUserDeletionJob : AsyncBackgroundJob q.UserName == args.UserName); if (keycloakUser == null) { _logger.LogError($"Keycloak user could not be found to delete! Username:{args.UserName}"); @@ -52,7 +52,6 @@ public class IdentityUserDeletionArgs public IdentityUserDeletionArgs() { - } public IdentityUserDeletionArgs(string userName) diff --git a/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/Users/KeycloakUserUpdatingJob.cs b/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/Users/KeycloakUserUpdatingJob.cs index 8bda0faf..5308ed5e 100644 --- a/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/Users/KeycloakUserUpdatingJob.cs +++ b/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/Users/KeycloakUserUpdatingJob.cs @@ -1,49 +1,70 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using EShopOnAbp.IdentityService.Keycloak; +using EShopOnAbp.IdentityService.Keycloak.Service; +using Keycloak.Net.Models.Users; using Microsoft.Extensions.Logging; using Volo.Abp; using Volo.Abp.BackgroundJobs; using Volo.Abp.DependencyInjection; +using Volo.Abp.ObjectMapping; namespace EShopOnAbp.IdentityService.BackgroundJobs.Users; public class KeycloakUserUpdatingJob : AsyncBackgroundJob, ITransientDependency { - private readonly KeycloakService _keycloakService; + private readonly IKeycloakService _keycloakService; private readonly ILogger _logger; + private readonly IObjectMapper _objectMapper; - public KeycloakUserUpdatingJob(KeycloakService keycloakService, ILogger logger) + public KeycloakUserUpdatingJob(IKeycloakService keycloakService, ILogger logger, IObjectMapper objectMapper) { _keycloakService = keycloakService; _logger = logger; + _objectMapper = objectMapper; } public override async Task ExecuteAsync(IdentityUserUpdatingArgs args) { try { - var keycloakUser = (await _keycloakService.GetUsersAsync(username: args.UserName)) - .First(); + var keycloakUser = (await _keycloakService.GetUsersAsync()) + .FirstOrDefault(q => q.UserName == args.UserName); if (keycloakUser == null) { _logger.LogError($"Keycloak user could not be found to update! Username:{args.UserName}"); throw new UserFriendlyException($"Keycloak user with the username:{args.UserName} could not be found!"); } - keycloakUser.UserName = args.UserName; - keycloakUser.Email = args.Email; - keycloakUser.FirstName = args.Name; - keycloakUser.LastName = args.Surname; - keycloakUser.Enabled = args.IsActive; - keycloakUser.EmailVerified = args.EmailConfirmed; + IEnumerable differentFields = args.GetDifferentFields().ToList(); + foreach (var fieldChange in differentFields) + { + if (fieldChange.FieldName == "Email") + keycloakUser.Email = fieldChange.NewValue.ToString(); + if (fieldChange.FieldName == "UserName") + keycloakUser.UserName = fieldChange.NewValue.ToString(); + if (fieldChange.FieldName == "Name") + keycloakUser.FirstName = fieldChange.NewValue.ToString(); + if (fieldChange.FieldName == "Surname") + keycloakUser.LastName = fieldChange.NewValue.ToString(); + if (fieldChange.FieldName == "IsActive") + keycloakUser.Enabled = (bool)fieldChange.NewValue; + if (fieldChange.FieldName == "RoleNames") + keycloakUser.RealmRoles = (string[])fieldChange.NewValue; + } - var result = - await _keycloakService.UpdateUserAsync(keycloakUser.Id, keycloakUser); - if (result) + if (differentFields.Count() != 0) { - _logger.LogInformation($"Keycloak user with the username:{args.UserName} has been updated."); + var mappedUser = _objectMapper.Map(keycloakUser); + var result = await _keycloakService.UpdateUserAsync( + keycloakUser.Id, + mappedUser + ); + if (result) + { + _logger.LogInformation($"Keycloak user with the username:{args.UserName} has been updated."); + } } } catch (Exception e) @@ -58,10 +79,52 @@ public class KeycloakUserUpdatingJob : AsyncBackgroundJob GetDifferentFields() + { + List fieldChanges = new List(); + + if ((Email, OldEmail) is not (null, null) && Email != OldEmail) + fieldChanges.Add(new FieldChange { FieldName = nameof(Email), NewValue = Email, OldValue = OldEmail }); + + if ((UserName, OldUserName) is not (null, null) && UserName != OldUserName) + fieldChanges.Add(new FieldChange + { FieldName = nameof(UserName), NewValue = UserName, OldValue = OldUserName }); + + if ((Name, OldName) is not (null, null) && Name != OldName) + fieldChanges.Add(new FieldChange { FieldName = nameof(Name), NewValue = Name, OldValue = OldName }); + + if ((Surname, OldSurname) is not (null, null) && Surname != OldSurname) + fieldChanges.Add(new FieldChange + { FieldName = nameof(Surname), NewValue = Surname, OldValue = OldSurname }); + + if (IsActive != OldIsActive) + fieldChanges.Add(new FieldChange + { FieldName = nameof(IsActive), NewValue = IsActive, OldValue = OldIsActive }); + + if (!Enumerable.SequenceEqual(RoleNames ?? Enumerable.Empty(), + OldRoleNames ?? Enumerable.Empty())) + fieldChanges.Add(new FieldChange + { FieldName = nameof(RoleNames), NewValue = RoleNames, OldValue = OldRoleNames }); + + return fieldChanges; + } + + public class FieldChange + { + public string FieldName { get; set; } + public object NewValue { get; set; } + public object OldValue { get; set; } + } } \ No newline at end of file diff --git a/services/identity/src/EShopOnAbp.IdentityService.Application/EShopOnAbpIdentityServiceAutoMapperProfile.cs b/services/identity/src/EShopOnAbp.IdentityService.Application/EShopOnAbpIdentityServiceAutoMapperProfile.cs new file mode 100644 index 00000000..3146db25 --- /dev/null +++ b/services/identity/src/EShopOnAbp.IdentityService.Application/EShopOnAbpIdentityServiceAutoMapperProfile.cs @@ -0,0 +1,21 @@ +using AutoMapper; +using EShopOnAbp.IdentityService.Keycloak.Service; +using Keycloak.Net.Models.Roles; +using Keycloak.Net.Models.Users; + +namespace EShopOnAbp.IdentityService; + +public class EShopOnAbpIdentityServiceAutoMapperProfile : Profile +{ + public EShopOnAbpIdentityServiceAutoMapperProfile() + { + CreateMap().ReverseMap(); + CreateMap().ReverseMap(); + CreateMap().ReverseMap(); + CreateMap().ReverseMap(); + CreateMap(); + + CreateMap().ReverseMap(); + CreateMap().ReverseMap(); + } +} \ No newline at end of file diff --git a/services/identity/src/EShopOnAbp.IdentityService.Application/EventHandlers/Roles/KeycloakRolesEventHandler.cs b/services/identity/src/EShopOnAbp.IdentityService.Application/EventHandlers/Roles/KeycloakRolesEventHandler.cs new file mode 100644 index 00000000..f5ffe8e4 --- /dev/null +++ b/services/identity/src/EShopOnAbp.IdentityService.Application/EventHandlers/Roles/KeycloakRolesEventHandler.cs @@ -0,0 +1,47 @@ +// using System; +// using System.Linq; +// using System.Threading.Tasks; +// using EShopOnAbp.IdentityService.Keycloak; +// using Microsoft.Extensions.Logging; +// using Volo.Abp.DependencyInjection; +// using Volo.Abp.Domain.Entities.Events.Distributed; +// using Volo.Abp.EventBus.Distributed; +// using Volo.Abp.Identity; +// +// namespace EShopOnAbp.IdentityService.EventHandlers.Roles; +// +// public class KeycloakRolesEventHandler : IDistributedEventHandler>, +// ITransientDependency +// { +// private readonly KeycloakService _keycloakService; +// private readonly ILogger _logger; +// +// public KeycloakRolesEventHandler(KeycloakService keycloakService, ILogger logger) +// { +// _keycloakService = keycloakService; +// _logger = logger; +// } +// +// public async Task HandleEventAsync(EntityCreatedEto eventData) +// { +// try +// { +// var existingRole = (await _keycloakService.GetRolesAsync()).FirstOrDefault(q => q.Name == eventData.Entity.Name); +// if (existingRole != null) +// { +// return; +// } +// +// var isSuccess = await _keycloakService.CreateRoleAsync(eventData.Entity.Name); +// if (isSuccess) +// { +// _logger.LogInformation($"Role created:{eventData.Entity.Name}"); +// } +// } +// catch (Exception e) +// { +// _logger.LogError($"Keycloak role creation with the name:{eventData.Entity.Name} failed!"); +// throw; +// } +// } +// } \ No newline at end of file diff --git a/services/identity/src/EShopOnAbp.IdentityService.Application/Identity/EShopIdentityRoleAppService.cs b/services/identity/src/EShopOnAbp.IdentityService.Application/Identity/EShopIdentityRoleAppService.cs index 20d7c5e1..61a3840e 100644 --- a/services/identity/src/EShopOnAbp.IdentityService.Application/Identity/EShopIdentityRoleAppService.cs +++ b/services/identity/src/EShopOnAbp.IdentityService.Application/Identity/EShopIdentityRoleAppService.cs @@ -31,9 +31,12 @@ public class EShopIdentityRoleAppService : IdentityRoleAppService public override async Task UpdateAsync(Guid id, IdentityRoleUpdateDto input) { + var role = await _identityRoleManager.GetByIdAsync(id); + var existingRoleName = role.Name; var result = await base.UpdateAsync(id, input); - await _backgroundJobManager.EnqueueAsync(new IdentityRoleUpdatingArgs(result.Name)); - + + await _backgroundJobManager.EnqueueAsync(new IdentityRoleUpdatingArgs(existingRoleName, input.Name)); + return result; } diff --git a/services/identity/src/EShopOnAbp.IdentityService.Application/Identity/EShopIdentityUserAppService.cs b/services/identity/src/EShopOnAbp.IdentityService.Application/Identity/EShopIdentityUserAppService.cs index 8a370a23..2f10bfe7 100644 --- a/services/identity/src/EShopOnAbp.IdentityService.Application/Identity/EShopIdentityUserAppService.cs +++ b/services/identity/src/EShopOnAbp.IdentityService.Application/Identity/EShopIdentityUserAppService.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Threading.Tasks; using EShopOnAbp.IdentityService.BackgroundJobs.Users; using Microsoft.AspNetCore.Identity; @@ -12,8 +13,9 @@ namespace EShopOnAbp.IdentityService.Identity; [ExposeServices(typeof(IdentityUserAppService), typeof(IIdentityUserAppService))] public class EShopIdentityUserAppService : IdentityUserAppService { - private readonly IdentityUserManager _userManager; + private readonly IIdentityUserRepository _userRepository; private readonly IBackgroundJobManager _backgroundJobManager; + private readonly IIdentityRoleRepository _roleRepository; public EShopIdentityUserAppService( IdentityUserManager userManager, @@ -25,7 +27,8 @@ public class EShopIdentityUserAppService : IdentityUserAppService roleRepository, identityOptions) { - _userManager = userManager; + _userRepository = userRepository; + _roleRepository = roleRepository; _backgroundJobManager = backgroundJobManager; } @@ -39,28 +42,46 @@ public class EShopIdentityUserAppService : IdentityUserAppService public override async Task UpdateAsync(Guid id, IdentityUserUpdateDto input) { + var existingUser = await _userRepository.GetAsync(id); + var args = await CreateIdentityUserUpdatingArgsAsync(existingUser, input); var updatedUser = await base.UpdateAsync(id, input); - await _backgroundJobManager.EnqueueAsync(new IdentityUserUpdatingArgs - { - Email = updatedUser.Email, - UserName = updatedUser.UserName, - Name = updatedUser.Name, - Surname = updatedUser.Surname, - EmailConfirmed = updatedUser.EmailConfirmed, - IsActive = input.IsActive, - RoleNames = input.RoleNames - }); + await _backgroundJobManager.EnqueueAsync(args); return updatedUser; } public override async Task DeleteAsync(Guid id) { - var user = await _userManager.FindByIdAsync(id.ToString()); + var user = await _userRepository.FindAsync(id); await base.DeleteAsync(id); if (user != null) { await _backgroundJobManager.EnqueueAsync(new IdentityUserDeletionArgs(user.UserName)); } } + + private async Task CreateIdentityUserUpdatingArgsAsync(IdentityUser existingUser, + IdentityUserUpdateDto input) + { + var userRoles = existingUser.Roles.Select(q => q.RoleId).ToList(); + var roles = await _roleRepository.GetListAsync(); + + var args = new IdentityUserUpdatingArgs + { + Email = input.Email, + OldEmail = existingUser.Email, + UserName = input.UserName, + OldUserName = existingUser.UserName, + Name = input.Name, + OldName = existingUser.Name, + Surname = input.Surname, + OldSurname = existingUser.Surname, + IsActive = input.IsActive, + OldIsActive = existingUser.IsActive, + RoleNames = input.RoleNames, + OldRoleNames = roles.Where(q => userRoles.Contains(q.Id)).Select(q => q.Name).ToArray() + }; + + return args; + } } \ No newline at end of file diff --git a/services/identity/src/EShopOnAbp.IdentityService.Application/IdentityServiceApplicationModule.cs b/services/identity/src/EShopOnAbp.IdentityService.Application/IdentityServiceApplicationModule.cs index cd2aacb1..7c9e2fd2 100644 --- a/services/identity/src/EShopOnAbp.IdentityService.Application/IdentityServiceApplicationModule.cs +++ b/services/identity/src/EShopOnAbp.IdentityService.Application/IdentityServiceApplicationModule.cs @@ -1,4 +1,5 @@ using EShopOnAbp.IdentityService.Keycloak; +using EShopOnAbp.IdentityService.Keycloak.Service; using Microsoft.Extensions.DependencyInjection; using Volo.Abp.AutoMapper; using Volo.Abp.BackgroundJobs; diff --git a/services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/KeycloakService.cs b/services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/KeycloakService.cs deleted file mode 100644 index 175d5d7d..00000000 --- a/services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/KeycloakService.cs +++ /dev/null @@ -1,85 +0,0 @@ -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Keycloak.Net; -using Keycloak.Net.Models.Roles; -using Keycloak.Net.Models.Users; -using Microsoft.Extensions.Options; -using Volo.Abp.DependencyInjection; - -namespace EShopOnAbp.IdentityService.Keycloak; - -public class KeycloakService : ITransientDependency -{ - private readonly KeycloakClient _keycloakClient; - private readonly KeycloakClientOptions _keycloakOptions; - - public KeycloakService(IOptions keycloakOptions) - { - _keycloakOptions = keycloakOptions.Value; - - _keycloakClient = new KeycloakClient( - _keycloakOptions.Url, - _keycloakOptions.AdminUserName, - _keycloakOptions.AdminPassword - ); - } - - public Task> GetUsersAsync(string search = null, string username = null, string email = null, - CancellationToken cancellationToken = default) - { - return _keycloakClient.GetUsersAsync(_keycloakOptions.RealmName, search: search, username: username, - email: email, cancellationToken: cancellationToken); - } - - public Task GetUserAsync(string userId, CancellationToken cancellationToken = default) - { - return _keycloakClient.GetUserAsync(_keycloakOptions.RealmName, userId, cancellationToken: cancellationToken); - } - - public Task CreateUserAsync(User user, CancellationToken cancellationToken = default) - { - return _keycloakClient.CreateUserAsync(_keycloakOptions.RealmName, user, cancellationToken); - } - - public Task UpdateUserAsync(string userId, User user, CancellationToken cancellationToken = default) - { - return _keycloakClient.UpdateUserAsync(_keycloakOptions.RealmName, userId, user, cancellationToken); - } - - public Task DeleteUserAsync(string userId, CancellationToken cancellationToken = default) - { - return _keycloakClient.DeleteUserAsync(_keycloakOptions.RealmName, userId, cancellationToken); - } - - public Task> GetRolesAsync(CancellationToken cancellationToken = default) - { - return _keycloakClient.GetRolesAsync(_keycloakOptions.RealmName, cancellationToken: cancellationToken); - } - - public Task AddRolesToUserAsync(string userId, IEnumerable roles, - CancellationToken cancellationToken = default) - { - return _keycloakClient.AddRealmRoleMappingsToUserAsync(_keycloakOptions.RealmName, userId, roles, - cancellationToken); - } - - public Task CreateRoleAsync(string name, CancellationToken cancellationToken = default) - { - Role role = new Role - { - Name = name - }; - return _keycloakClient.CreateRoleAsync(_keycloakOptions.RealmName, role, cancellationToken); - } - - public Task DeleteRoleByIdAsync(string id, CancellationToken cancellationToken = default) - { - return _keycloakClient.DeleteRoleByIdAsync(_keycloakOptions.RealmName, id, cancellationToken); - } - - public Task UpdateRoleAsync(string id, Role role, CancellationToken cancellationToken = default) - { - return _keycloakClient.UpdateRoleByIdAsync(_keycloakOptions.RealmName, id, role, cancellationToken); - } -} \ No newline at end of file diff --git a/services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/Service/CachedKeycloakRole.cs b/services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/Service/CachedKeycloakRole.cs new file mode 100644 index 00000000..82e298e6 --- /dev/null +++ b/services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/Service/CachedKeycloakRole.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using Volo.Abp.Caching; + +namespace EShopOnAbp.IdentityService.Keycloak.Service; + +[CacheName("KeycloakRole")] +public class CachedKeycloakRole +{ + public string Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public bool? Composite { get; set; } + public CachedRoleComposite Composites { get; set; } + public bool? ClientRole { get; set; } + public string ContainerId { get; set; } + public IDictionary Attributes { get; set; } +} + +public class CachedRoleComposite +{ + public IDictionary Client { get; set; } + public IEnumerable Realm { get; set; } +} \ No newline at end of file diff --git a/services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/Service/CachedKeycloakService.cs b/services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/Service/CachedKeycloakService.cs new file mode 100644 index 00000000..64e06aec --- /dev/null +++ b/services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/Service/CachedKeycloakService.cs @@ -0,0 +1,127 @@ +// using System.Collections.Generic; +// using System.Linq; +// using System.Threading; +// using System.Threading.Tasks; +// using Keycloak.Net.Models.Roles; +// using Keycloak.Net.Models.Users; +// using Volo.Abp.Caching; +// using Volo.Abp.DependencyInjection; +// +// namespace EShopOnAbp.IdentityService.Keycloak.Service; +// +// [ExposeServices(typeof(CachedKeycloakService))] +// public class CachedKeycloakService : IKeycloakService +// { +// protected const string UsersCacheKey = "KeycloakUsers"; +// protected const string RolesCacheKey = "KeycloakRoles"; +// private readonly IKeycloakService _keycloakService; +// private readonly IDistributedCache> _keycloakUsersCache; +// private readonly IDistributedCache> _keycloakRolesCache; +// +// public CachedKeycloakService(IKeycloakService keycloakService, +// IDistributedCache> keycloakUsersCache, +// IDistributedCache> keycloakRolesCache) +// { +// _keycloakService = keycloakService; +// _keycloakUsersCache = keycloakUsersCache; +// _keycloakRolesCache = keycloakRolesCache; +// } +// +// public async Task> GetUsersAsync(string search = null, string username = null, +// string email = null, +// CancellationToken cancellationToken = default) +// { +// var users = await _keycloakUsersCache.GetAsync(UsersCacheKey, token: cancellationToken); +// if (users == null) +// { +// users = (await _keycloakService.GetUsersAsync(search, username, email, cancellationToken)).ToList(); +// await _keycloakUsersCache.SetAsync(UsersCacheKey, users, token: cancellationToken); +// } +// +// return users; +// } +// +// public async Task CreateUserAsync(User user, CancellationToken cancellationToken = default) +// { +// var result = await _keycloakService.CreateUserAsync(user, cancellationToken); +// if (result) +// { +// await _keycloakUsersCache.RemoveAsync(UsersCacheKey, token: cancellationToken); +// } +// +// return result; +// } +// +// public async Task UpdateUserAsync(string userId, User user, CancellationToken cancellationToken = default) +// { +// var result = await _keycloakService.UpdateUserAsync(userId, user, cancellationToken); +// if (result) +// { +// await _keycloakUsersCache.RemoveAsync(UsersCacheKey, token: cancellationToken); +// } +// +// return result; +// } +// +// public async Task DeleteUserAsync(string userId, CancellationToken cancellationToken = default) +// { +// var result = await _keycloakService.DeleteUserAsync(userId, cancellationToken); +// if (result) +// { +// await _keycloakUsersCache.RemoveAsync(UsersCacheKey, token: cancellationToken); +// } +// +// return result; +// } +// +// public async Task> GetRolesAsync(CancellationToken cancellationToken = default) +// { +// var roles = await _keycloakRolesCache.GetAsync(RolesCacheKey, token: cancellationToken); +// if (roles == null) +// { +// roles = (await _keycloakService.GetRolesAsync(cancellationToken: cancellationToken)).ToList(); +// await _keycloakRolesCache.SetAsync(RolesCacheKey, roles, token: cancellationToken); +// } +// +// return roles; +// } +// +// public Task AddRolesToUserAsync(string userId, IEnumerable roles, +// CancellationToken cancellationToken = default) +// { +// return _keycloakService.AddRolesToUserAsync(userId, roles, cancellationToken); +// } +// +// public async Task CreateRoleAsync(string name, CancellationToken cancellationToken = default) +// { +// var result = await _keycloakService.CreateRoleAsync(name, cancellationToken); +// if (result) +// { +// await _keycloakRolesCache.RemoveAsync(RolesCacheKey, token: cancellationToken); +// } +// +// return result; +// } +// +// public async Task DeleteRoleByIdAsync(string id, CancellationToken cancellationToken = default) +// { +// var result = await _keycloakService.DeleteRoleByIdAsync(id, cancellationToken); +// if (result) +// { +// await _keycloakRolesCache.RemoveAsync(RolesCacheKey, token: cancellationToken); +// } +// +// return result; +// } +// +// public async Task UpdateRoleAsync(string id, Role role, CancellationToken cancellationToken = default) +// { +// var result = await _keycloakService.UpdateRoleAsync(id, role, cancellationToken); +// if (result) +// { +// await _keycloakRolesCache.RemoveAsync(RolesCacheKey, token: cancellationToken); +// } +// +// return result; +// } +// } \ No newline at end of file diff --git a/services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/Service/CachedKeycloakUser.cs b/services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/Service/CachedKeycloakUser.cs new file mode 100644 index 00000000..671cd8f7 --- /dev/null +++ b/services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/Service/CachedKeycloakUser.cs @@ -0,0 +1,75 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using Volo.Abp.Caching; + +namespace EShopOnAbp.IdentityService.Keycloak.Service; + +[CacheName("KeycloakUser")] +public class CachedKeycloakUser +{ + public string Id { get; set; } + public long CreatedTimestamp { get; set; } + public string UserName { get; set; } + public bool? Enabled { get; set; } + public bool? Totp { get; set; } + public bool? EmailVerified { get; set; } + public string FirstName { get; set; } + public string LastName { get; set; } + public string Email { get; set; } + public Collection DisableableCredentialTypes { get; set; } + public Collection RequiredActions { get; set; } + public int? NotBefore { get; set; } + public Dictionary> Attributes { get; set; } + public IDictionary ClientRoles { get; set; } + public string FederationLink { get; set; } + public IEnumerable Groups { get; set; } + public string Origin { get; set; } + public string[] RealmRoles { get; set; } + public string Self { get; set; } + public string ServiceAccountClientId { get; set; } + public CachedUserAccess Access { get; set; } + public IEnumerable ClientConsents { get; set; } + public IEnumerable Credentials { get; set; } + public IEnumerable FederatedIdentities { get; set; } +} + +public class CachedUserConsent +{ + public string ClientId { get; set; } + public IEnumerable GrantedClientScopes { get; set; } + public long? CreatedDate { get; set; } + public long? LastUpdatedDate { get; set; } +} + +public class CachedUserAccess +{ + public bool? ManageGroupMembership { get; set; } + public bool? View { get; set; } + public bool? MapRoles { get; set; } + public bool? Impersonate { get; set; } + public bool? Manage { get; set; } +} + +public class CachedCredentials +{ + public string Algorithm { get; set; } + public IDictionary Config { get; set; } + public int? Counter { get; set; } + public long? CreatedDate { get; set; } + public string Device { get; set; } + public int? Digits { get; set; } + public int? HashIterations { get; set; } + public string HashSaltedValue { get; set; } + public int? Period { get; set; } + public string Salt { get; set; } + public bool? Temporary { get; set; } + public string Type { get; set; } + public string Value { get; set; } +} + +public class CachedFederatedIdentity +{ + public string IdentityProvider { get; set; } + public string UserId { get; set; } + public string UserName { get; set; } +} \ No newline at end of file diff --git a/services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/Service/IKeycloakService.cs b/services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/Service/IKeycloakService.cs new file mode 100644 index 00000000..12bf5b7e --- /dev/null +++ b/services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/Service/IKeycloakService.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Keycloak.Net.Models.Roles; +using Keycloak.Net.Models.Users; +using Volo.Abp.DependencyInjection; + +namespace EShopOnAbp.IdentityService.Keycloak.Service; + +/* + * This will be an external service from the Keycloak package. + * Keeping it under Application layer because the Keycloak.Net.Core package requires .Net6 target framework. + * Application.Contracts targets netstandard2.0 + */ +public interface IKeycloakService : ITransientDependency +{ + Task> GetUsersAsync(string search = null, string username = null, string email = null, CancellationToken cancellationToken = default); + + Task CreateUserAsync(User user, CancellationToken cancellationToken = default); + + Task UpdateUserAsync(string userId, User user, CancellationToken cancellationToken = default); + + Task DeleteUserAsync(string userId, CancellationToken cancellationToken = default); + + Task> GetRolesAsync(CancellationToken cancellationToken = default); + + Task AddRealmRolesToUserAsync(string userId, IEnumerable roles, CancellationToken cancellationToken = default); + public Task> GetRealmRolesOfUserAsync(string userId, CancellationToken cancellationToken = default); + + Task RemoveRealmRolesFromUserAsync(string userId, IEnumerable roles, CancellationToken cancellationToken = default); + + Task CreateRoleAsync(string name, CancellationToken cancellationToken = default); + + Task DeleteRoleByIdAsync(string id, CancellationToken cancellationToken = default); + + Task UpdateRoleAsync(string id, Role role, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/KeycloakClientOptions.cs b/services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/Service/KeycloakClientOptions.cs similarity index 78% rename from services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/KeycloakClientOptions.cs rename to services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/Service/KeycloakClientOptions.cs index 93850fa2..9498cf56 100644 --- a/services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/KeycloakClientOptions.cs +++ b/services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/Service/KeycloakClientOptions.cs @@ -1,4 +1,4 @@ -namespace EShopOnAbp.IdentityService.Keycloak; +namespace EShopOnAbp.IdentityService.Keycloak.Service; public class KeycloakClientOptions { public string Url { get; set; } diff --git a/services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/Service/KeycloakService.cs b/services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/Service/KeycloakService.cs new file mode 100644 index 00000000..23ed6caa --- /dev/null +++ b/services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/Service/KeycloakService.cs @@ -0,0 +1,182 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Keycloak.Net; +using Keycloak.Net.Models.Roles; +using Keycloak.Net.Models.Users; +using Microsoft.Extensions.Options; +using Volo.Abp.Caching; +using Volo.Abp.DependencyInjection; +using Volo.Abp.ObjectMapping; + +namespace EShopOnAbp.IdentityService.Keycloak.Service; + +/* + * This will be an external service from the Keycloak package + */ +[ExposeServices(typeof(IKeycloakService), typeof(KeycloakService))] +public class KeycloakService : IKeycloakService +{ + protected const string UsersCacheKey = "KeycloakUsers"; + protected const string RolesCacheKey = "KeycloakRoles"; + private readonly IObjectMapper _objectMapper; + private readonly IDistributedCache, string> _keycloakUsersCache; + private readonly IDistributedCache, string> _keycloakRolesCache; + private readonly IDistributedCache, string> _userRolesCache; + + private readonly KeycloakClient _keycloakClient; + private readonly KeycloakClientOptions _keycloakOptions; + + public KeycloakService( + IOptions keycloakOptions, + IObjectMapper objectMapper, + IDistributedCache, string> keycloakUsersCache, + IDistributedCache, string> keycloakRolesCache, + IDistributedCache, string> userRolesCache) + { + _objectMapper = objectMapper; + _keycloakUsersCache = keycloakUsersCache; + _keycloakRolesCache = keycloakRolesCache; + _userRolesCache = userRolesCache; + + _keycloakOptions = keycloakOptions.Value; + _keycloakClient = new KeycloakClient( + _keycloakOptions.Url, + _keycloakOptions.AdminUserName, + _keycloakOptions.AdminPassword + ); + } + + public async Task> GetUsersAsync(string search = null, string username = null, + string email = null, + CancellationToken cancellationToken = default) + { + var users = await _keycloakUsersCache.GetAsync(UsersCacheKey, token: cancellationToken); + if (users == null) + { + var result = await _keycloakClient.GetUsersAsync(_keycloakOptions.RealmName, search: search, + username: username, + email: email, cancellationToken: cancellationToken); + users = _objectMapper.Map, List>(result.ToList()); + await _keycloakUsersCache.SetAsync(UsersCacheKey, users, token: cancellationToken); + } + + return users; + } + + public async Task CreateUserAsync(User user, CancellationToken cancellationToken = default) + { + var result = await _keycloakClient.CreateUserAsync(_keycloakOptions.RealmName, user, cancellationToken); + if (result) + { + await _keycloakUsersCache.RemoveAsync(UsersCacheKey, token: cancellationToken); + } + + return result; + } + + public async Task UpdateUserAsync(string userId, User user, CancellationToken cancellationToken = default) + { + var result = await _keycloakClient.UpdateUserAsync(_keycloakOptions.RealmName, userId, user, cancellationToken); + if (result) + { + await _keycloakUsersCache.RemoveAsync(UsersCacheKey, token: cancellationToken); + } + +// _keycloakClient.DeleteRealmRoleMappingsFromUserAsync() + return result; + } + + public async Task DeleteUserAsync(string userId, CancellationToken cancellationToken = default) + { + var result = await _keycloakClient.DeleteUserAsync(_keycloakOptions.RealmName, userId, cancellationToken); + if (result) + { + await _keycloakUsersCache.RemoveAsync(UsersCacheKey, token: cancellationToken); + } + + return result; + } + + public async Task> GetRolesAsync(CancellationToken cancellationToken = default) + { + var roles = await _keycloakRolesCache.GetAsync(RolesCacheKey, token: cancellationToken); + if (roles == null) + { + var result = + (await _keycloakClient.GetRolesAsync(_keycloakOptions.RealmName, cancellationToken: cancellationToken)) + .ToList(); + roles = _objectMapper.Map, List>(result.ToList()); + + await _keycloakRolesCache.SetAsync(RolesCacheKey, roles, token: cancellationToken); + } + + return roles; + } + + public Task AddRealmRolesToUserAsync(string userId, IEnumerable roles, + CancellationToken cancellationToken = default) + { + return _keycloakClient.AddRealmRoleMappingsToUserAsync(_keycloakOptions.RealmName, userId, roles, + cancellationToken); + } + + // Updating user with roles is not working + public async Task> GetRealmRolesOfUserAsync(string userId, + CancellationToken cancellationToken = default) + { + var userRoles = await _userRolesCache.GetAsync(userId, token: cancellationToken); + if (userRoles == null) + { + var roles = (await _keycloakClient.GetRealmRoleMappingsForUserAsync(_keycloakOptions.RealmName, userId, + cancellationToken)) + .ToList(); + userRoles = _objectMapper.Map, List>(roles); + await _userRolesCache.SetAsync(userId, userRoles, token: cancellationToken); + } + + return userRoles; + } + + public Task RemoveRealmRolesFromUserAsync(string userId, IEnumerable roles, + CancellationToken cancellationToken = default) + { + return _keycloakClient.DeleteRealmRoleMappingsFromUserAsync(_keycloakOptions.RealmName, userId, roles, + cancellationToken); + } + + public async Task CreateRoleAsync(string name, CancellationToken cancellationToken = default) + { + var result = await _keycloakClient.CreateRoleAsync(_keycloakOptions.RealmName, new Role() { Name = name }, + cancellationToken); + if (result) + { + await _keycloakRolesCache.RemoveAsync(RolesCacheKey, token: cancellationToken); + } + + return result; + } + + public async Task DeleteRoleByIdAsync(string id, CancellationToken cancellationToken = default) + { + var result = await _keycloakClient.DeleteRoleByIdAsync(_keycloakOptions.RealmName, id, cancellationToken); + if (result) + { + await _keycloakRolesCache.RemoveAsync(RolesCacheKey, token: cancellationToken); + } + + return result; + } + + public async Task UpdateRoleAsync(string id, Role role, CancellationToken cancellationToken = default) + { + var result = await _keycloakClient.UpdateRoleByIdAsync(_keycloakOptions.RealmName, id, role, cancellationToken); + if (result) + { + await _keycloakRolesCache.RemoveAsync(RolesCacheKey, token: cancellationToken); + } + + return result; + } +} \ No newline at end of file diff --git a/services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/Service/KeycloakServiceExtensions.cs b/services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/Service/KeycloakServiceExtensions.cs new file mode 100644 index 00000000..01474c1b --- /dev/null +++ b/services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/Service/KeycloakServiceExtensions.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; +using Keycloak.Net.Models.Roles; +using Keycloak.Net.Models.Users; + +namespace EShopOnAbp.IdentityService.Keycloak.Service; + +public static class KeycloakServiceExtensions +{ + public static string GenerateCacheKeyBasedOnValues(this IEnumerable roles) + { + return GenerateUniqueCacheKeyBasedOnList(roles); + } + + public static string GenerateCacheKeyBasedOnValues(this IEnumerable users) + { + return GenerateUniqueCacheKeyBasedOnList(users); + } + + private static string GenerateUniqueCacheKeyBasedOnList(IEnumerable list) + { + string serializedList = JsonSerializer.Serialize(list); + byte[] bytes = Encoding.UTF8.GetBytes(serializedList); + byte[] hash = SHA256.Create().ComputeHash(bytes); + string hashString = BitConverter.ToString(hash).Replace("-", ""); + return hashString; + } +} \ No newline at end of file