Browse Source

Merge pull request #180 from abpframework/gterdem/keycloak_sync

Account management and User management issues
pull/183/head
Halil İbrahim Kalkan 3 years ago
committed by GitHub
parent
commit
1c6a447680
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      .env.example
  2. 42
      services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/Roles/KeycloakRoleCreationJob.cs
  3. 42
      services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/Roles/KeycloakRoleDeletionJob.cs
  4. 55
      services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/Roles/KeycloakRoleUpdatingJob.cs
  5. 104
      services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/Users/KeycloakUserCreationJob.cs
  6. 61
      services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/Users/KeycloakUserDeletionJob.cs
  7. 156
      services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/Users/KeycloakUserUpdatingJob.cs
  8. 2
      services/identity/src/EShopOnAbp.IdentityService.Application/EShopOnAbp.IdentityService.Application.csproj
  9. 21
      services/identity/src/EShopOnAbp.IdentityService.Application/EShopOnAbpIdentityServiceAutoMapperProfile.cs
  10. 47
      services/identity/src/EShopOnAbp.IdentityService.Application/EventHandlers/Roles/KeycloakRolesEventHandler.cs
  11. 54
      services/identity/src/EShopOnAbp.IdentityService.Application/Identity/EShopIdentityRoleAppService.cs
  12. 92
      services/identity/src/EShopOnAbp.IdentityService.Application/Identity/EShopIdentityUserAppService.cs
  13. 23
      services/identity/src/EShopOnAbp.IdentityService.Application/IdentityServiceApplicationModule.cs
  14. 23
      services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/Service/CachedKeycloakRole.cs
  15. 75
      services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/Service/CachedKeycloakUser.cs
  16. 36
      services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/Service/IKeycloakService.cs
  17. 8
      services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/Service/KeycloakClientOptions.cs
  18. 161
      services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/Service/KeycloakService.cs
  19. 32
      services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/Service/KeycloakServiceExtensions.cs
  20. 6
      services/identity/src/EShopOnAbp.IdentityService.HttpApi.Host/appsettings.json
  21. 2
      shared/EShopOnAbp.Keycloak.DbMigrator/KeycloakDataSeeder.cs

2
.env.example

@ -1,2 +0,0 @@
#Payment__PayPal__ClientId=PAYPAL_CLIENT_ID
#Payment__PayPal__Secret=PAYPAL_SECRET

42
services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/Roles/KeycloakRoleCreationJob.cs

@ -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);

42
services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/Roles/KeycloakRoleDeletionJob.cs

@ -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);

55
services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/Roles/KeycloakRoleUpdatingJob.cs

@ -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);

104
services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/Users/KeycloakUserCreationJob.cs

@ -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;
}
}

61
services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/Users/KeycloakUserDeletionJob.cs

@ -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;
}
}

156
services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/Users/KeycloakUserUpdatingJob.cs

@ -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; }
}
}

2
services/identity/src/EShopOnAbp.IdentityService.Application/EShopOnAbp.IdentityService.Application.csproj

@ -11,7 +11,9 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Volo.Abp.BackgroundJobs" Version="7.0.0" />
<PackageReference Include="Volo.Abp.Identity.Application" Version="7.0.0" /> <PackageReference Include="Volo.Abp.Identity.Application" Version="7.0.0" />
<PackageReference Include="Keycloak.Net.Core" Version="1.0.20" />
</ItemGroup> </ItemGroup>
</Project> </Project>

21
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<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();
}
}

47
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<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;
// }
// }
// }

54
services/identity/src/EShopOnAbp.IdentityService.Application/Identity/EShopIdentityRoleAppService.cs

@ -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));
}
}

92
services/identity/src/EShopOnAbp.IdentityService.Application/Identity/EShopIdentityUserAppService.cs

@ -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;
}
}

23
services/identity/src/EShopOnAbp.IdentityService.Application/IdentityServiceApplicationModule.cs

@ -1,5 +1,8 @@
using Microsoft.Extensions.DependencyInjection; using EShopOnAbp.IdentityService.Keycloak;
using EShopOnAbp.IdentityService.Keycloak.Service;
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.AutoMapper; using Volo.Abp.AutoMapper;
using Volo.Abp.BackgroundJobs;
using Volo.Abp.Identity; using Volo.Abp.Identity;
using Volo.Abp.Modularity; using Volo.Abp.Modularity;
@ -8,17 +11,29 @@ namespace EShopOnAbp.IdentityService
[DependsOn( [DependsOn(
typeof(IdentityServiceDomainModule), typeof(IdentityServiceDomainModule),
typeof(IdentityServiceApplicationContractsModule), typeof(IdentityServiceApplicationContractsModule),
typeof(AbpIdentityApplicationModule) typeof(AbpIdentityApplicationModule),
)] typeof(AbpBackgroundJobsModule)
)]
public class IdentityServiceApplicationModule : AbpModule public class IdentityServiceApplicationModule : AbpModule
{ {
public override void ConfigureServices(ServiceConfigurationContext context) public override void ConfigureServices(ServiceConfigurationContext context)
{ {
var configuration = context.Services.GetConfiguration();
context.Services.AddAutoMapperObjectMapper<IdentityServiceApplicationModule>(); context.Services.AddAutoMapperObjectMapper<IdentityServiceApplicationModule>();
Configure<AbpAutoMapperOptions>(options => Configure<AbpAutoMapperOptions>(options =>
{ {
options.AddMaps<IdentityServiceApplicationModule>(validate: true); options.AddMaps<IdentityServiceApplicationModule>(validate: true);
}); });
Configure<KeycloakClientOptions>(options =>
{
options.Url = configuration["Keycloak:url"];
options.AdminUserName = configuration["Keycloak:adminUsername"];
options.AdminPassword = configuration["Keycloak:adminPassword"];
options.RealmName = configuration["Keycloak:realmName"];
}
);
} }
} }
} }

23
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<string, object> Attributes { get; set; }
}
public class CachedRoleComposite
{
public IDictionary<string, string> Client { get; set; }
public IEnumerable<string> Realm { get; set; }
}

75
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<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; }
}

36
services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/Service/IKeycloakService.cs

@ -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);
}

8
services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/Service/KeycloakClientOptions.cs

@ -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; }
}

161
services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/Service/KeycloakService.cs

@ -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;
}
}

32
services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/Service/KeycloakServiceExtensions.cs

@ -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;
}
}

6
services/identity/src/EShopOnAbp.IdentityService.HttpApi.Host/appsettings.json

@ -9,6 +9,12 @@
"SwaggerClientId": "WebGateway_Swagger", "SwaggerClientId": "WebGateway_Swagger",
"SwaggerClientSecret": "1q2w3e*" "SwaggerClientSecret": "1q2w3e*"
}, },
"Keycloak": {
"url": "http://localhost:8080",
"adminUsername": "admin",
"adminPassword": "1q2w3E*",
"realmName": "master"
},
"Logging": { "Logging": {
"LogLevel": { "LogLevel": {
"Default": "Information", "Default": "Information",

2
shared/EShopOnAbp.Keycloak.DbMigrator/KeycloakDataSeeder.cs

@ -39,7 +39,7 @@ public class KeyCloakDataSeeder : IDataSeedContributor, ITransientDependency
{ {
await UpdateRealmSettingsAsync(); await UpdateRealmSettingsAsync();
await UpdateAdminUserAsync(); await UpdateAdminUserAsync();
await CreateRoleMapperAsync(); await CreateRoleMapperAsync(); // roles scope
await CreateClientScopesAsync(); await CreateClientScopesAsync();
await CreateClientsAsync(); await CreateClientsAsync();
} }

Loading…
Cancel
Save