committed by
GitHub
21 changed files with 1037 additions and 7 deletions
@ -1,2 +0,0 @@ |
|||||
#Payment__PayPal__ClientId=PAYPAL_CLIENT_ID |
|
||||
#Payment__PayPal__Secret=PAYPAL_SECRET |
|
||||
@ -0,0 +1,42 @@ |
|||||
|
using System; |
||||
|
using System.Linq; |
||||
|
using System.Threading.Tasks; |
||||
|
using EShopOnAbp.IdentityService.Keycloak.Service; |
||||
|
using Microsoft.Extensions.Logging; |
||||
|
using Volo.Abp.BackgroundJobs; |
||||
|
using Volo.Abp.DependencyInjection; |
||||
|
|
||||
|
namespace EShopOnAbp.IdentityService.BackgroundJobs.Roles; |
||||
|
|
||||
|
public class KeycloakRoleCreationJob : AsyncBackgroundJob<IdentityRoleCreationArgs>, ITransientDependency |
||||
|
{ |
||||
|
private readonly IKeycloakService _keycloakService; |
||||
|
private readonly ILogger<KeycloakRoleCreationJob> _logger; |
||||
|
|
||||
|
public KeycloakRoleCreationJob(IKeycloakService keycloakService, ILogger<KeycloakRoleCreationJob> logger) |
||||
|
{ |
||||
|
_keycloakService = keycloakService; |
||||
|
_logger = logger; |
||||
|
} |
||||
|
|
||||
|
public override async Task ExecuteAsync(IdentityRoleCreationArgs args) |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
var existingRole = (await _keycloakService.GetRolesAsync()).FirstOrDefault(q => q.Name == args.Name); |
||||
|
if (existingRole != null) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
await _keycloakService.CreateRoleAsync(args.Name); |
||||
|
} |
||||
|
catch (Exception e) |
||||
|
{ |
||||
|
_logger.LogWarning($"Keycloak role creation with the name:{args.Name} failed!"); |
||||
|
throw; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public record IdentityRoleCreationArgs(string Name); |
||||
@ -0,0 +1,42 @@ |
|||||
|
using System; |
||||
|
using System.Linq; |
||||
|
using System.Threading.Tasks; |
||||
|
using EShopOnAbp.IdentityService.Keycloak.Service; |
||||
|
using Microsoft.Extensions.Logging; |
||||
|
using Volo.Abp.BackgroundJobs; |
||||
|
using Volo.Abp.DependencyInjection; |
||||
|
|
||||
|
namespace EShopOnAbp.IdentityService.BackgroundJobs.Roles; |
||||
|
|
||||
|
public class KeycloakRoleDeletionJob : AsyncBackgroundJob<IdentityRoleDeletionArgs>, ITransientDependency |
||||
|
{ |
||||
|
private readonly IKeycloakService _keycloakService; |
||||
|
private readonly ILogger<KeycloakRoleDeletionJob> _logger; |
||||
|
|
||||
|
public KeycloakRoleDeletionJob(IKeycloakService keycloakService, ILogger<KeycloakRoleDeletionJob> logger) |
||||
|
{ |
||||
|
_keycloakService = keycloakService; |
||||
|
_logger = logger; |
||||
|
} |
||||
|
|
||||
|
public override async Task ExecuteAsync(IdentityRoleDeletionArgs args) |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
var existingRole = (await _keycloakService.GetRolesAsync()).FirstOrDefault(q => q.Name == args.Name); |
||||
|
if (existingRole == null) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
await _keycloakService.DeleteRoleByIdAsync(existingRole.Id); |
||||
|
} |
||||
|
catch (Exception e) |
||||
|
{ |
||||
|
_logger.LogWarning($"Could not delete the role with the name:{args.Name} from Keycloak server!"); |
||||
|
throw; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public record IdentityRoleDeletionArgs(string Name); |
||||
@ -0,0 +1,55 @@ |
|||||
|
using System; |
||||
|
using System.Linq; |
||||
|
using System.Threading.Tasks; |
||||
|
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<IdentityRoleUpdatingArgs>, ITransientDependency |
||||
|
{ |
||||
|
private readonly IKeycloakService _keycloakService; |
||||
|
private readonly ILogger<KeycloakRoleUpdatingJob> _logger; |
||||
|
private readonly IObjectMapper _objectMapper; |
||||
|
|
||||
|
public KeycloakRoleUpdatingJob(IKeycloakService keycloakService, ILogger<KeycloakRoleUpdatingJob> 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.OldName); |
||||
|
if (existingRole == null) |
||||
|
{ |
||||
|
_logger.LogWarning($"Role with the name:{args.OldName} couldn't be found to update!"); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
if (args.OldName != args.NewName) |
||||
|
{ |
||||
|
existingRole.Name = args.NewName; |
||||
|
|
||||
|
await _keycloakService.UpdateRoleAsync(existingRole.Id, |
||||
|
_objectMapper.Map<CachedKeycloakRole, Role>(existingRole) |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
catch (Exception e) |
||||
|
{ |
||||
|
_logger.LogWarning($"Could not update the role with the name:{args.OldName} from Keycloak server!"); |
||||
|
throw; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public record IdentityRoleUpdatingArgs(string OldName, string NewName); |
||||
@ -0,0 +1,104 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using System.Threading.Tasks; |
||||
|
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<IdentityUserCreationArgs>, ITransientDependency |
||||
|
{ |
||||
|
private readonly IKeycloakService _keycloakService; |
||||
|
private readonly ILogger _logger; |
||||
|
private readonly IObjectMapper _objectMapper; |
||||
|
|
||||
|
public KeycloakUserCreationJob(IKeycloakService keycloakService, |
||||
|
ILogger<KeycloakUserCreationJob> logger, IObjectMapper objectMapper) |
||||
|
{ |
||||
|
_logger = logger; |
||||
|
_objectMapper = objectMapper; |
||||
|
_keycloakService = keycloakService; |
||||
|
} |
||||
|
|
||||
|
public override async Task ExecuteAsync(IdentityUserCreationArgs args) |
||||
|
{ |
||||
|
var keycloakUser = new User |
||||
|
{ |
||||
|
Email = args.Email, |
||||
|
UserName = args.UserName, |
||||
|
FirstName = args.Name, |
||||
|
LastName = args.Surname, |
||||
|
Enabled = args.IsActive, |
||||
|
Credentials = new List<Credentials>() |
||||
|
{ |
||||
|
new() { Type = "password", Value = args.Password } |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
try |
||||
|
{ |
||||
|
var result = await _keycloakService.CreateUserAsync(keycloakUser); |
||||
|
if (result) |
||||
|
{ |
||||
|
if (args.RoleNames.Length != 0) |
||||
|
{ |
||||
|
await AddRolesToKeycloakUserAsync(keycloakUser.UserName, args.RoleNames); |
||||
|
} |
||||
|
|
||||
|
_logger.LogInformation($"Keycloak user with the username:{args.UserName} has been created."); |
||||
|
} |
||||
|
} |
||||
|
catch (Exception e) |
||||
|
{ |
||||
|
_logger.LogError($"Keycloak user creation with the Username:{args.UserName} has been failed!"); |
||||
|
throw; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private async Task AddRolesToKeycloakUserAsync(string userName, string[] roleNames) |
||||
|
{ |
||||
|
var user = (await _keycloakService.GetUsersAsync()).FirstOrDefault(q=>q.UserName == userName); |
||||
|
var allTheRoles = await _keycloakService.GetRolesAsync(); |
||||
|
var roles = allTheRoles.Where(q => roleNames.Contains(q.Name)).ToList(); |
||||
|
|
||||
|
await _keycloakService.AddRealmRolesToUserAsync( |
||||
|
user.Id, |
||||
|
_objectMapper.Map<List<CachedKeycloakRole>,List<Role>>(roles) |
||||
|
); |
||||
|
_logger.LogInformation($"Keycloak roles:{roleNames} has been added to user with the username:{userName}."); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public class IdentityUserCreationArgs |
||||
|
{ |
||||
|
public string Email { get; init; } |
||||
|
public string UserName { get; init; } |
||||
|
public string Name { get; init; } |
||||
|
public string Surname { get; init; } |
||||
|
public string Password { get; init; } |
||||
|
public bool IsActive { get; init; } |
||||
|
public string[] RoleNames { get; init; } |
||||
|
|
||||
|
public IdentityUserCreationArgs() // For deserialization
|
||||
|
{ |
||||
|
|
||||
|
} |
||||
|
|
||||
|
public IdentityUserCreationArgs(IdentityUserCreateDto input) |
||||
|
{ |
||||
|
Email = input.Email; |
||||
|
UserName = input.UserName; |
||||
|
Name = input.Name; |
||||
|
Surname = input.Surname; |
||||
|
Password = input.Password; |
||||
|
IsActive = input.IsActive; |
||||
|
RoleNames = input.RoleNames; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,61 @@ |
|||||
|
using System; |
||||
|
using System.Linq; |
||||
|
using System.Threading.Tasks; |
||||
|
using EShopOnAbp.IdentityService.Keycloak.Service; |
||||
|
using Microsoft.Extensions.Logging; |
||||
|
using Volo.Abp; |
||||
|
using Volo.Abp.BackgroundJobs; |
||||
|
using Volo.Abp.DependencyInjection; |
||||
|
|
||||
|
namespace EShopOnAbp.IdentityService.BackgroundJobs.Users; |
||||
|
|
||||
|
public class KeycloakUserDeletionJob : AsyncBackgroundJob<IdentityUserDeletionArgs>, ITransientDependency |
||||
|
{ |
||||
|
private readonly IKeycloakService _keycloakService; |
||||
|
private readonly ILogger _logger; |
||||
|
|
||||
|
public KeycloakUserDeletionJob(IKeycloakService keycloakService, |
||||
|
ILogger<KeycloakUserCreationJob> logger) |
||||
|
{ |
||||
|
_keycloakService = keycloakService; |
||||
|
_logger = logger; |
||||
|
} |
||||
|
|
||||
|
public override async Task ExecuteAsync(IdentityUserDeletionArgs args) |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
var keycloakUser = (await _keycloakService.GetUsersAsync()) |
||||
|
.FirstOrDefault(q => q.UserName == args.UserName); |
||||
|
if (keycloakUser == null) |
||||
|
{ |
||||
|
_logger.LogError($"Keycloak user could not be found to delete! Username:{args.UserName}"); |
||||
|
throw new UserFriendlyException($"Keycloak user with the username:{args.UserName} could not be found!"); |
||||
|
} |
||||
|
|
||||
|
var result = await _keycloakService.DeleteUserAsync(keycloakUser.Id); |
||||
|
if (result) |
||||
|
{ |
||||
|
_logger.LogInformation($"Keycloak user with the username:{args.UserName} has been deleted."); |
||||
|
} |
||||
|
} |
||||
|
catch (Exception e) |
||||
|
{ |
||||
|
_logger.LogError($"Keycloak user deletion failed! Username:{args.UserName}"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public class IdentityUserDeletionArgs |
||||
|
{ |
||||
|
public string UserName { get; init; } |
||||
|
|
||||
|
public IdentityUserDeletionArgs() |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
public IdentityUserDeletionArgs(string userName) |
||||
|
{ |
||||
|
UserName = userName; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,156 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using System.Threading.Tasks; |
||||
|
using EShopOnAbp.IdentityService.Keycloak.Service; |
||||
|
using Keycloak.Net.Models.Roles; |
||||
|
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<IdentityUserUpdatingArgs>, ITransientDependency |
||||
|
{ |
||||
|
private readonly IKeycloakService _keycloakService; |
||||
|
private readonly ILogger<KeycloakUserCreationJob> _logger; |
||||
|
private readonly IObjectMapper _objectMapper; |
||||
|
|
||||
|
public KeycloakUserUpdatingJob(IKeycloakService keycloakService, ILogger<KeycloakUserCreationJob> logger, |
||||
|
IObjectMapper objectMapper) |
||||
|
{ |
||||
|
_keycloakService = keycloakService; |
||||
|
_logger = logger; |
||||
|
_objectMapper = objectMapper; |
||||
|
} |
||||
|
|
||||
|
public override async Task ExecuteAsync(IdentityUserUpdatingArgs args) |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
var keycloakUser = (await _keycloakService.GetUsersAsync()) |
||||
|
.FirstOrDefault(q => q.UserName == args.OldUserName); |
||||
|
if (keycloakUser == null) |
||||
|
{ |
||||
|
_logger.LogError($"Keycloak user could not be found to update! Username:{args.OldUserName}"); |
||||
|
throw new UserFriendlyException($"Keycloak user with the username:{args.OldUserName} could not be found!"); |
||||
|
} |
||||
|
|
||||
|
IEnumerable<IdentityUserUpdatingArgs.FieldChange> differentFields = args.GetDifferentFields().ToList(); |
||||
|
foreach (var fieldChange in differentFields) |
||||
|
{ |
||||
|
if (fieldChange.FieldName == "Email") |
||||
|
keycloakUser.Email = fieldChange.NewValue.ToString(); |
||||
|
if (fieldChange.FieldName == "UserName") // Username update is not working - not updating in keycloak
|
||||
|
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; |
||||
|
} |
||||
|
|
||||
|
if (differentFields.Count() != 0) |
||||
|
{ |
||||
|
var mappedUser = _objectMapper.Map<CachedKeycloakUser, User>(keycloakUser); |
||||
|
|
||||
|
var result = await _keycloakService.UpdateUserAsync( |
||||
|
keycloakUser.Id, |
||||
|
mappedUser |
||||
|
); |
||||
|
|
||||
|
// User roles are not being updated - Updating manually
|
||||
|
if (differentFields.FirstOrDefault(q => q.FieldName == "RoleNames") != null) |
||||
|
{ |
||||
|
var oldRoles = (await _keycloakService.GetRolesAsync()) |
||||
|
.Where(q => args.OldRoleNames.Contains(q.Name)) |
||||
|
.ToList(); |
||||
|
var newRoles = (await _keycloakService.GetRolesAsync()) |
||||
|
.Where(q => args.RoleNames.Contains(q.Name)) |
||||
|
.ToList(); |
||||
|
if (oldRoles.Count > 0) |
||||
|
{ |
||||
|
await _keycloakService.RemoveRealmRolesFromUserAsync(keycloakUser.Id, |
||||
|
_objectMapper.Map<List<CachedKeycloakRole>, List<Role>>(oldRoles)); |
||||
|
} |
||||
|
|
||||
|
if (newRoles.Count > 0) |
||||
|
{ |
||||
|
await _keycloakService.AddRealmRolesToUserAsync(keycloakUser.Id, |
||||
|
_objectMapper.Map<List<CachedKeycloakRole>, List<Role>>(newRoles)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (result) |
||||
|
{ |
||||
|
_logger.LogInformation($"Keycloak user with the username:{args.UserName} has been updated."); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
catch (Exception e) |
||||
|
{ |
||||
|
_logger.LogWarning($"Keycloak user updating failed! Username:{args.UserName}"); |
||||
|
throw new UserFriendlyException($"Keycloak user updating failed! Username:{args.UserName}", |
||||
|
innerException: e); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public class IdentityUserUpdatingArgs |
||||
|
{ |
||||
|
public string Email { get; init; } |
||||
|
public string OldEmail { get; init; } |
||||
|
public string UserName { get; init; } |
||||
|
public string OldUserName { get; init; } |
||||
|
public string Name { get; init; } |
||||
|
public string OldName { get; init; } |
||||
|
public string Surname { get; init; } |
||||
|
public string OldSurname { get; init; } |
||||
|
public bool IsActive { get; init; } |
||||
|
public bool OldIsActive { get; init; } |
||||
|
public string[] RoleNames { get; init; } |
||||
|
public string[] OldRoleNames { get; init; } |
||||
|
|
||||
|
public IEnumerable<FieldChange> GetDifferentFields() |
||||
|
{ |
||||
|
List<FieldChange> fieldChanges = new List<FieldChange>(); |
||||
|
|
||||
|
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<string>(), |
||||
|
OldRoleNames ?? Enumerable.Empty<string>())) |
||||
|
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; } |
||||
|
} |
||||
|
} |
||||
@ -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<User, CachedKeycloakUser>().ReverseMap(); |
||||
|
CreateMap<UserAccess, CachedUserAccess>().ReverseMap(); |
||||
|
CreateMap<UserConsent, CachedUserConsent>().ReverseMap(); |
||||
|
CreateMap<Credentials, CachedCredentials>().ReverseMap(); |
||||
|
CreateMap<FederatedIdentity, CachedFederatedIdentity>(); |
||||
|
|
||||
|
CreateMap<Role, CachedKeycloakRole>().ReverseMap(); |
||||
|
CreateMap<RoleComposite, CachedRoleComposite>().ReverseMap(); |
||||
|
} |
||||
|
} |
||||
@ -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<EntityCreatedEto<IdentityRoleEto>>,
|
||||
|
// ITransientDependency
|
||||
|
// {
|
||||
|
// private readonly KeycloakService _keycloakService;
|
||||
|
// private readonly ILogger<KeycloakRolesEventHandler> _logger;
|
||||
|
//
|
||||
|
// public KeycloakRolesEventHandler(KeycloakService keycloakService, ILogger<KeycloakRolesEventHandler> logger)
|
||||
|
// {
|
||||
|
// _keycloakService = keycloakService;
|
||||
|
// _logger = logger;
|
||||
|
// }
|
||||
|
//
|
||||
|
// public async Task HandleEventAsync(EntityCreatedEto<IdentityRoleEto> 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;
|
||||
|
// }
|
||||
|
// }
|
||||
|
// }
|
||||
@ -0,0 +1,54 @@ |
|||||
|
using System; |
||||
|
using System.Threading.Tasks; |
||||
|
using EShopOnAbp.IdentityService.BackgroundJobs.Roles; |
||||
|
using Volo.Abp.BackgroundJobs; |
||||
|
using Volo.Abp.DependencyInjection; |
||||
|
using Volo.Abp.Identity; |
||||
|
|
||||
|
namespace EShopOnAbp.IdentityService.Identity; |
||||
|
|
||||
|
[ExposeServices(typeof(IdentityRoleAppService), typeof(IIdentityRoleAppService))] |
||||
|
public class EShopIdentityRoleAppService : IdentityRoleAppService |
||||
|
{ |
||||
|
private readonly IBackgroundJobManager _backgroundJobManager; |
||||
|
private readonly IdentityRoleManager _identityRoleManager; |
||||
|
|
||||
|
public EShopIdentityRoleAppService(IdentityRoleManager roleManager, IIdentityRoleRepository roleRepository, |
||||
|
IBackgroundJobManager backgroundJobManager, IdentityRoleManager identityRoleManager) : base( |
||||
|
roleManager, roleRepository) |
||||
|
{ |
||||
|
_backgroundJobManager = backgroundJobManager; |
||||
|
_identityRoleManager = identityRoleManager; |
||||
|
} |
||||
|
|
||||
|
public override async Task<IdentityRoleDto> CreateAsync(IdentityRoleCreateDto input) |
||||
|
{ |
||||
|
var result = await base.CreateAsync(input); |
||||
|
await _backgroundJobManager.EnqueueAsync(new IdentityRoleCreationArgs(result.Name)); |
||||
|
|
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
public override async Task<IdentityRoleDto> 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(existingRoleName, input.Name)); |
||||
|
|
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
public override async Task DeleteAsync(Guid id) |
||||
|
{ |
||||
|
var existingRole = await _identityRoleManager.FindByIdAsync(id.ToString()); |
||||
|
await base.DeleteAsync(id); |
||||
|
if (existingRole == null) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
await _backgroundJobManager.EnqueueAsync(new IdentityRoleDeletionArgs(existingRole.Name)); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,92 @@ |
|||||
|
using System; |
||||
|
using System.Linq; |
||||
|
using System.Threading.Tasks; |
||||
|
using EShopOnAbp.IdentityService.BackgroundJobs.Users; |
||||
|
using Microsoft.AspNetCore.Identity; |
||||
|
using Microsoft.Extensions.Options; |
||||
|
using Volo.Abp.BackgroundJobs; |
||||
|
using Volo.Abp.DependencyInjection; |
||||
|
using Volo.Abp.Identity; |
||||
|
|
||||
|
namespace EShopOnAbp.IdentityService.Identity; |
||||
|
|
||||
|
[ExposeServices(typeof(IdentityUserAppService), typeof(IIdentityUserAppService))] |
||||
|
public class EShopIdentityUserAppService : IdentityUserAppService |
||||
|
{ |
||||
|
private readonly IIdentityUserRepository _userRepository; |
||||
|
private readonly IBackgroundJobManager _backgroundJobManager; |
||||
|
private readonly IIdentityRoleRepository _roleRepository; |
||||
|
|
||||
|
public EShopIdentityUserAppService( |
||||
|
IdentityUserManager userManager, |
||||
|
IIdentityUserRepository userRepository, |
||||
|
IIdentityRoleRepository roleRepository, |
||||
|
IOptions<IdentityOptions> identityOptions, |
||||
|
IBackgroundJobManager backgroundJobManager) : base(userManager, |
||||
|
userRepository, |
||||
|
roleRepository, |
||||
|
identityOptions) |
||||
|
{ |
||||
|
_userRepository = userRepository; |
||||
|
_roleRepository = roleRepository; |
||||
|
_backgroundJobManager = backgroundJobManager; |
||||
|
} |
||||
|
|
||||
|
public override async Task<IdentityUserDto> CreateAsync(IdentityUserCreateDto input) |
||||
|
{ |
||||
|
var createdUser = await base.CreateAsync(input); |
||||
|
await _backgroundJobManager.EnqueueAsync(new IdentityUserCreationArgs(input)); |
||||
|
|
||||
|
return createdUser; |
||||
|
} |
||||
|
|
||||
|
public override async Task<IdentityUserDto> UpdateAsync(Guid id, IdentityUserUpdateDto input) |
||||
|
{ |
||||
|
var existingUser = await _userRepository.GetAsync(id); |
||||
|
// Disabling username updating. Keycloak service is unavailable to update the username field!
|
||||
|
if (input.UserName != existingUser.UserName) |
||||
|
{ |
||||
|
input.UserName = existingUser.UserName; |
||||
|
} |
||||
|
var args = await CreateIdentityUserUpdatingArgsAsync(existingUser, input); |
||||
|
var updatedUser = await base.UpdateAsync(id, input); |
||||
|
await _backgroundJobManager.EnqueueAsync(args); |
||||
|
|
||||
|
return updatedUser; |
||||
|
} |
||||
|
|
||||
|
public override async Task DeleteAsync(Guid id) |
||||
|
{ |
||||
|
var user = await _userRepository.FindAsync(id); |
||||
|
await base.DeleteAsync(id); |
||||
|
if (user != null) |
||||
|
{ |
||||
|
await _backgroundJobManager.EnqueueAsync(new IdentityUserDeletionArgs(user.UserName)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private async Task<IdentityUserUpdatingArgs> 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; |
||||
|
} |
||||
|
} |
||||
@ -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<string, object> Attributes { get; set; } |
||||
|
} |
||||
|
|
||||
|
public class CachedRoleComposite |
||||
|
{ |
||||
|
public IDictionary<string, string> Client { get; set; } |
||||
|
public IEnumerable<string> Realm { get; set; } |
||||
|
} |
||||
@ -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<string> DisableableCredentialTypes { get; set; } |
||||
|
public Collection<string> RequiredActions { get; set; } |
||||
|
public int? NotBefore { get; set; } |
||||
|
public Dictionary<string, IEnumerable<string>> Attributes { get; set; } |
||||
|
public IDictionary<string, object> ClientRoles { get; set; } |
||||
|
public string FederationLink { get; set; } |
||||
|
public IEnumerable<string> 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<CachedUserConsent> ClientConsents { get; set; } |
||||
|
public IEnumerable<CachedCredentials> Credentials { get; set; } |
||||
|
public IEnumerable<CachedFederatedIdentity> FederatedIdentities { get; set; } |
||||
|
} |
||||
|
|
||||
|
public class CachedUserConsent |
||||
|
{ |
||||
|
public string ClientId { get; set; } |
||||
|
public IEnumerable<string> 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<string, string> 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; } |
||||
|
} |
||||
@ -0,0 +1,36 @@ |
|||||
|
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<List<CachedKeycloakUser>> GetUsersAsync(string search = null, string username = null, string email = null, CancellationToken cancellationToken = default); |
||||
|
|
||||
|
Task<bool> CreateUserAsync(User user, CancellationToken cancellationToken = default); |
||||
|
|
||||
|
Task<bool> UpdateUserAsync(string userId, User user, CancellationToken cancellationToken = default); |
||||
|
|
||||
|
Task<bool> DeleteUserAsync(string userId, CancellationToken cancellationToken = default); |
||||
|
|
||||
|
Task<List<CachedKeycloakRole>> GetRolesAsync(CancellationToken cancellationToken = default); |
||||
|
|
||||
|
Task<bool> AddRealmRolesToUserAsync(string userId, IEnumerable<Role> roles, CancellationToken cancellationToken = default); |
||||
|
|
||||
|
Task<bool> RemoveRealmRolesFromUserAsync(string userId, IEnumerable<Role> roles, CancellationToken cancellationToken = default); |
||||
|
|
||||
|
Task<bool> CreateRoleAsync(string name, CancellationToken cancellationToken = default); |
||||
|
|
||||
|
Task<bool> DeleteRoleByIdAsync(string id, CancellationToken cancellationToken = default); |
||||
|
|
||||
|
Task<bool> UpdateRoleAsync(string id, Role role, CancellationToken cancellationToken = default); |
||||
|
} |
||||
@ -0,0 +1,8 @@ |
|||||
|
namespace EShopOnAbp.IdentityService.Keycloak.Service; |
||||
|
public class KeycloakClientOptions |
||||
|
{ |
||||
|
public string Url { get; set; } |
||||
|
public string AdminUserName { get; set; } |
||||
|
public string AdminPassword { get; set; } |
||||
|
public string RealmName { get; set; } |
||||
|
} |
||||
@ -0,0 +1,161 @@ |
|||||
|
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<List<CachedKeycloakUser>, string> _keycloakUsersCache; |
||||
|
private readonly IDistributedCache<List<CachedKeycloakRole>, string> _keycloakRolesCache; |
||||
|
|
||||
|
private readonly KeycloakClient _keycloakClient; |
||||
|
private readonly KeycloakClientOptions _keycloakOptions; |
||||
|
|
||||
|
public KeycloakService( |
||||
|
IOptions<KeycloakClientOptions> keycloakOptions, |
||||
|
IObjectMapper objectMapper, |
||||
|
IDistributedCache<List<CachedKeycloakUser>, string> keycloakUsersCache, |
||||
|
IDistributedCache<List<CachedKeycloakRole>, string> keycloakRolesCache) |
||||
|
{ |
||||
|
_objectMapper = objectMapper; |
||||
|
_keycloakUsersCache = keycloakUsersCache; |
||||
|
_keycloakRolesCache = keycloakRolesCache; |
||||
|
|
||||
|
_keycloakOptions = keycloakOptions.Value; |
||||
|
_keycloakClient = new KeycloakClient( |
||||
|
_keycloakOptions.Url, |
||||
|
_keycloakOptions.AdminUserName, |
||||
|
_keycloakOptions.AdminPassword |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
public async Task<List<CachedKeycloakUser>> 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<User>, List<CachedKeycloakUser>>(result.ToList()); |
||||
|
await _keycloakUsersCache.SetAsync(UsersCacheKey, users, token: cancellationToken); |
||||
|
} |
||||
|
|
||||
|
return users; |
||||
|
} |
||||
|
|
||||
|
public async Task<bool> 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<bool> 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); |
||||
|
} |
||||
|
|
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
public async Task<bool> 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<List<CachedKeycloakRole>> 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<Role>, List<CachedKeycloakRole>>(result.ToList()); |
||||
|
|
||||
|
await _keycloakRolesCache.SetAsync(RolesCacheKey, roles, token: cancellationToken); |
||||
|
} |
||||
|
|
||||
|
return roles; |
||||
|
} |
||||
|
|
||||
|
public Task<bool> AddRealmRolesToUserAsync(string userId, IEnumerable<Role> roles, |
||||
|
CancellationToken cancellationToken = default) |
||||
|
{ |
||||
|
return _keycloakClient.AddRealmRoleMappingsToUserAsync(_keycloakOptions.RealmName, userId, roles, |
||||
|
cancellationToken); |
||||
|
} |
||||
|
|
||||
|
public Task<bool> RemoveRealmRolesFromUserAsync(string userId, IEnumerable<Role> roles, |
||||
|
CancellationToken cancellationToken = default) |
||||
|
{ |
||||
|
return _keycloakClient.DeleteRealmRoleMappingsFromUserAsync(_keycloakOptions.RealmName, userId, roles, |
||||
|
cancellationToken); |
||||
|
} |
||||
|
|
||||
|
public async Task<bool> 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<bool> 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<bool> 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; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,32 @@ |
|||||
|
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; |
||||
|
|
||||
|
/* Extensions to create unique strings based on list values */ |
||||
|
public static class KeycloakServiceExtensions |
||||
|
{ |
||||
|
public static string GenerateCacheKeyBasedOnValues(this IEnumerable<Role> roles) |
||||
|
{ |
||||
|
return GenerateUniqueCacheKeyBasedOnList(roles); |
||||
|
} |
||||
|
|
||||
|
public static string GenerateCacheKeyBasedOnValues(this IEnumerable<User> users) |
||||
|
{ |
||||
|
return GenerateUniqueCacheKeyBasedOnList(users); |
||||
|
} |
||||
|
|
||||
|
private static string GenerateUniqueCacheKeyBasedOnList<T>(IEnumerable<T> 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; |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue