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