From 37c6f2460d556770393db6dbcf19f261c701f211 Mon Sep 17 00:00:00 2001 From: Galip Tolga Erdem Date: Tue, 18 Jul 2023 23:19:55 -0400 Subject: [PATCH 1/9] Added background jobs for identity user changes --- .../BackgroundJobs/IdentityUserDeletionJob.cs | 60 ++++++++++++++++ .../BackgroundJobs/KeycloakClientOptions.cs | 9 +++ .../BackgroundJobs/KeycloakUserCreationJob.cs | 70 ++++++++++++++++++ .../BackgroundJobs/KeycloakUserUpdatingJob.cs | 70 ++++++++++++++++++ .../EShopIdentityUserAppService.cs | 71 +++++++++++++++++++ ...opOnAbp.IdentityService.Application.csproj | 2 + .../IdentityServiceApplicationModule.cs | 22 ++++-- .../appsettings.json | 6 ++ 8 files changed, 306 insertions(+), 4 deletions(-) create mode 100644 services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/IdentityUserDeletionJob.cs create mode 100644 services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/KeycloakClientOptions.cs create mode 100644 services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/KeycloakUserCreationJob.cs create mode 100644 services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/KeycloakUserUpdatingJob.cs create mode 100644 services/identity/src/EShopOnAbp.IdentityService.Application/EShopIdentityUserAppService.cs diff --git a/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/IdentityUserDeletionJob.cs b/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/IdentityUserDeletionJob.cs new file mode 100644 index 00000000..21206599 --- /dev/null +++ b/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/IdentityUserDeletionJob.cs @@ -0,0 +1,60 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Keycloak.Net; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Volo.Abp; +using Volo.Abp.BackgroundJobs; +using Volo.Abp.DependencyInjection; + +namespace EShopOnAbp.IdentityService.BackgroundJobs; + +public class IdentityUserDeletionJob : AsyncBackgroundJob, ITransientDependency +{ + private readonly KeycloakClient _keycloakClient; + private readonly KeycloakClientOptions _keycloakOptions; + private readonly ILogger _logger; + + public IdentityUserDeletionJob(IOptions keycloakOptions, + ILogger logger) + { + _logger = logger; + _keycloakOptions = keycloakOptions.Value; + + _keycloakClient = new KeycloakClient( + _keycloakOptions.Url, + _keycloakOptions.AdminUserName, + _keycloakOptions.AdminPassword + ); + } + + public override async Task ExecuteAsync(IdentityUserDeletionArgs args) + { + try + { + var keycloakUser = (await _keycloakClient.GetUsersAsync(_keycloakOptions.RealmName, username: args.UserName)) + .First(); + 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 _keycloakClient.DeleteUserAsync(_keycloakOptions.RealmName, 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; set; } +} \ No newline at end of file diff --git a/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/KeycloakClientOptions.cs b/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/KeycloakClientOptions.cs new file mode 100644 index 00000000..648a3823 --- /dev/null +++ b/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/KeycloakClientOptions.cs @@ -0,0 +1,9 @@ +namespace EShopOnAbp.IdentityService.BackgroundJobs; + +public class KeycloakClientOptions +{ + public string Url { get; set; } + public string AdminUserName { get; set; } + public string AdminPassword { get; set; } + public string RealmName { get; set; } +} \ No newline at end of file diff --git a/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/KeycloakUserCreationJob.cs b/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/KeycloakUserCreationJob.cs new file mode 100644 index 00000000..1da1cbca --- /dev/null +++ b/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/KeycloakUserCreationJob.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Keycloak.Net; +using Keycloak.Net.Models.Users; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Volo.Abp.BackgroundJobs; +using Volo.Abp.DependencyInjection; + +namespace EShopOnAbp.IdentityService.BackgroundJobs; + +public class KeycloakUserCreationJob : AsyncBackgroundJob, ITransientDependency +{ + private readonly KeycloakClient _keycloakClient; + private readonly KeycloakClientOptions _keycloakOptions; + private readonly ILogger _logger; + + public KeycloakUserCreationJob(IOptions keycloakOptions, + ILogger logger) + { + _logger = logger; + _keycloakOptions = keycloakOptions.Value; + + _keycloakClient = new KeycloakClient( + _keycloakOptions.Url, + _keycloakOptions.AdminUserName, + _keycloakOptions.AdminPassword + ); + } + + public override async Task ExecuteAsync(IdentityUserCreationArgs args) + { + var keycloakUser = new User + { + Email = args.Email, + UserName = args.UserName, + FirstName = args.Name, + LastName = args.Surname, + Enabled = true, + Credentials = new List() + { + new() { Type = "password", Value = args.Password } + } + }; + + try + { + var result = await _keycloakClient.CreateUserAsync(_keycloakOptions.RealmName, keycloakUser); + if (result) + { + _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; + } + } +} + +public class IdentityUserCreationArgs +{ + public string Email { get; set; } + public string UserName { get; set; } + public string Name { get; set; } + public string Surname { get; set; } + public string Password { get; set; } +} \ No newline at end of file diff --git a/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/KeycloakUserUpdatingJob.cs b/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/KeycloakUserUpdatingJob.cs new file mode 100644 index 00000000..17d82e38 --- /dev/null +++ b/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/KeycloakUserUpdatingJob.cs @@ -0,0 +1,70 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Keycloak.Net; +using Keycloak.Net.Models.Users; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Volo.Abp; +using Volo.Abp.BackgroundJobs; +using Volo.Abp.DependencyInjection; + +namespace EShopOnAbp.IdentityService.BackgroundJobs; + +public class KeycloakUserUpdatingJob : AsyncBackgroundJob, ITransientDependency +{ + private readonly KeycloakClient _keycloakClient; + private readonly KeycloakClientOptions _keycloakOptions; + private readonly ILogger _logger; + + public KeycloakUserUpdatingJob(IOptions keycloakOptions, ILogger logger) + { + _logger = logger; + _keycloakOptions = keycloakOptions.Value; + + _keycloakClient = new KeycloakClient( + _keycloakOptions.Url, + _keycloakOptions.AdminUserName, + _keycloakOptions.AdminPassword + ); + } + + public override async Task ExecuteAsync(IdentityUserUpdatingArgs args) + { + try + { + var keycloakUser = (await _keycloakClient.GetUsersAsync(_keycloakOptions.RealmName, username: args.UserName)) + .First(); + if (keycloakUser == null) + { + _logger.LogError($"Keycloak user could not be found to update! Username:{args.UserName}"); + throw new UserFriendlyException($"Keycloak user with the username:{args.UserName} could not be found!"); + } + + keycloakUser.UserName = args.UserName; + keycloakUser.Email = args.Email; + keycloakUser.FirstName = args.Name; + keycloakUser.LastName = args.Surname; + keycloakUser.EmailVerified = args.EmailConfirmed; + + var result = await _keycloakClient.UpdateUserAsync(_keycloakOptions.RealmName, keycloakUser.Id, keycloakUser); + if (result) + { + _logger.LogInformation($"Keycloak user with the username:{args.UserName} has been updated."); + } + } + catch (Exception e) + { + _logger.LogError($"Keycloak user updating failed! Username:{args.UserName}"); + } + } +} + +public class IdentityUserUpdatingArgs +{ + public string Email { get; set; } + public string UserName { get; set; } + public string Name { get; set; } + public string Surname { get; set; } + public bool EmailConfirmed { get; set; } +} \ No newline at end of file diff --git a/services/identity/src/EShopOnAbp.IdentityService.Application/EShopIdentityUserAppService.cs b/services/identity/src/EShopOnAbp.IdentityService.Application/EShopIdentityUserAppService.cs new file mode 100644 index 00000000..673720e7 --- /dev/null +++ b/services/identity/src/EShopOnAbp.IdentityService.Application/EShopIdentityUserAppService.cs @@ -0,0 +1,71 @@ +using System; +using System.Threading.Tasks; +using EShopOnAbp.IdentityService.BackgroundJobs; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Options; +using Volo.Abp.BackgroundJobs; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Identity; + +namespace EShopOnAbp.IdentityService; + +[ExposeServices(typeof(IdentityUserAppService), typeof(IIdentityUserAppService))] +public class EShopIdentityUserAppService : IdentityUserAppService +{ + private readonly IBackgroundJobManager _backgroundJobManager; + private readonly IIdentityUserRepository _userRepository; + + public EShopIdentityUserAppService( + IdentityUserManager userManager, + IIdentityUserRepository userRepository, + IIdentityRoleRepository roleRepository, + IOptions identityOptions, IBackgroundJobManager backgroundJobManager) : base(userManager, + userRepository, + roleRepository, + identityOptions) + { + _backgroundJobManager = backgroundJobManager; + _userRepository = userRepository; + } + + public override async Task CreateAsync(IdentityUserCreateDto input) + { + var createdUser = await base.CreateAsync(input); + await _backgroundJobManager.EnqueueAsync(new IdentityUserCreationArgs + { + Email = createdUser.Email, + UserName = createdUser.UserName, + Name = createdUser.Name, + Surname = createdUser.Surname, + Password = input.Password + }); + + return createdUser; + } + + public override async Task UpdateAsync(Guid id, IdentityUserUpdateDto input) + { + var updatedUser = await base.UpdateAsync(id, input); + await _backgroundJobManager.EnqueueAsync(new IdentityUserUpdatingArgs + { + Email = updatedUser.Email, + UserName = updatedUser.UserName, + Name = updatedUser.Name, + Surname = updatedUser.Surname, + EmailConfirmed = updatedUser.EmailConfirmed + }); + + return updatedUser; + } + + public override async Task DeleteAsync(Guid id) + { + var user = await _userRepository.FindAsync(id); + await base.DeleteAsync(id); + + await _backgroundJobManager.EnqueueAsync(new IdentityUserDeletionArgs + { + UserName = user.UserName + }); + } +} \ No newline at end of file diff --git a/services/identity/src/EShopOnAbp.IdentityService.Application/EShopOnAbp.IdentityService.Application.csproj b/services/identity/src/EShopOnAbp.IdentityService.Application/EShopOnAbp.IdentityService.Application.csproj index 9a5c929f..bb85acac 100644 --- a/services/identity/src/EShopOnAbp.IdentityService.Application/EShopOnAbp.IdentityService.Application.csproj +++ b/services/identity/src/EShopOnAbp.IdentityService.Application/EShopOnAbp.IdentityService.Application.csproj @@ -11,7 +11,9 @@ + + diff --git a/services/identity/src/EShopOnAbp.IdentityService.Application/IdentityServiceApplicationModule.cs b/services/identity/src/EShopOnAbp.IdentityService.Application/IdentityServiceApplicationModule.cs index 502d3733..c0b7e66f 100644 --- a/services/identity/src/EShopOnAbp.IdentityService.Application/IdentityServiceApplicationModule.cs +++ b/services/identity/src/EShopOnAbp.IdentityService.Application/IdentityServiceApplicationModule.cs @@ -1,5 +1,7 @@ -using Microsoft.Extensions.DependencyInjection; +using EShopOnAbp.IdentityService.BackgroundJobs; +using Microsoft.Extensions.DependencyInjection; using Volo.Abp.AutoMapper; +using Volo.Abp.BackgroundJobs; using Volo.Abp.Identity; using Volo.Abp.Modularity; @@ -8,17 +10,29 @@ namespace EShopOnAbp.IdentityService [DependsOn( typeof(IdentityServiceDomainModule), typeof(IdentityServiceApplicationContractsModule), - typeof(AbpIdentityApplicationModule) - )] + typeof(AbpIdentityApplicationModule), + typeof(AbpBackgroundJobsModule) + )] public class IdentityServiceApplicationModule : AbpModule { public override void ConfigureServices(ServiceConfigurationContext context) { + var configuration = context.Services.GetConfiguration(); + context.Services.AddAutoMapperObjectMapper(); Configure(options => { options.AddMaps(validate: true); }); + + Configure(options => + { + options.Url = configuration["Keycloak:url"]; + options.AdminUserName = configuration["Keycloak:adminUsername"]; + options.AdminPassword = configuration["Keycloak:adminPassword"]; + options.RealmName = configuration["Keycloak:realmName"]; + } + ); } } -} +} \ No newline at end of file diff --git a/services/identity/src/EShopOnAbp.IdentityService.HttpApi.Host/appsettings.json b/services/identity/src/EShopOnAbp.IdentityService.HttpApi.Host/appsettings.json index 9cdfa28f..f37922ad 100644 --- a/services/identity/src/EShopOnAbp.IdentityService.HttpApi.Host/appsettings.json +++ b/services/identity/src/EShopOnAbp.IdentityService.HttpApi.Host/appsettings.json @@ -9,6 +9,12 @@ "SwaggerClientId": "WebGateway_Swagger", "SwaggerClientSecret": "1q2w3e*" }, + "Keycloak": { + "url": "http://localhost:8080", + "adminUsername": "admin", + "adminPassword": "1q2w3E*", + "realmName": "master" + }, "Logging": { "LogLevel": { "Default": "Information", From bb13320209597e012b3d65fdc050545944e22e0c Mon Sep 17 00:00:00 2001 From: Galip Tolga Erdem Date: Mon, 24 Jul 2023 17:19:35 -0400 Subject: [PATCH 2/9] Added KeycloakService --- .../BackgroundJobs/KeycloakClientOptions.cs | 9 --- .../IdentityServiceApplicationModule.cs | 2 +- .../Keycloak/KeycloakClientOptions.cs | 8 +++ .../Keycloak/KeycloakService.cs | 66 +++++++++++++++++++ 4 files changed, 75 insertions(+), 10 deletions(-) delete mode 100644 services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/KeycloakClientOptions.cs create mode 100644 services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/KeycloakClientOptions.cs create mode 100644 services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/KeycloakService.cs diff --git a/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/KeycloakClientOptions.cs b/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/KeycloakClientOptions.cs deleted file mode 100644 index 648a3823..00000000 --- a/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/KeycloakClientOptions.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace EShopOnAbp.IdentityService.BackgroundJobs; - -public class KeycloakClientOptions -{ - public string Url { get; set; } - public string AdminUserName { get; set; } - public string AdminPassword { get; set; } - public string RealmName { get; set; } -} \ No newline at end of file diff --git a/services/identity/src/EShopOnAbp.IdentityService.Application/IdentityServiceApplicationModule.cs b/services/identity/src/EShopOnAbp.IdentityService.Application/IdentityServiceApplicationModule.cs index c0b7e66f..cd2aacb1 100644 --- a/services/identity/src/EShopOnAbp.IdentityService.Application/IdentityServiceApplicationModule.cs +++ b/services/identity/src/EShopOnAbp.IdentityService.Application/IdentityServiceApplicationModule.cs @@ -1,4 +1,4 @@ -using EShopOnAbp.IdentityService.BackgroundJobs; +using EShopOnAbp.IdentityService.Keycloak; using Microsoft.Extensions.DependencyInjection; using Volo.Abp.AutoMapper; using Volo.Abp.BackgroundJobs; diff --git a/services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/KeycloakClientOptions.cs b/services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/KeycloakClientOptions.cs new file mode 100644 index 00000000..93850fa2 --- /dev/null +++ b/services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/KeycloakClientOptions.cs @@ -0,0 +1,8 @@ +namespace EShopOnAbp.IdentityService.Keycloak; + public class KeycloakClientOptions + { + public string Url { get; set; } + public string AdminUserName { get; set; } + public string AdminPassword { get; set; } + public string RealmName { get; set; } + } \ No newline at end of file diff --git a/services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/KeycloakService.cs b/services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/KeycloakService.cs new file mode 100644 index 00000000..19da1314 --- /dev/null +++ b/services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/KeycloakService.cs @@ -0,0 +1,66 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Keycloak.Net; +using Keycloak.Net.Models.Roles; +using Keycloak.Net.Models.Users; +using Microsoft.Extensions.Options; +using Volo.Abp.DependencyInjection; + +namespace EShopOnAbp.IdentityService.Keycloak; + +public class KeycloakService : ITransientDependency +{ + private readonly KeycloakClient _keycloakClient; + private readonly KeycloakClientOptions _keycloakOptions; + + public KeycloakService(IOptions keycloakOptions) + { + _keycloakOptions = keycloakOptions.Value; + + _keycloakClient = new KeycloakClient( + _keycloakOptions.Url, + _keycloakOptions.AdminUserName, + _keycloakOptions.AdminPassword + ); + } + + public Task> GetUsersAsync(string search = null, string username = null, string email = null, + CancellationToken cancellationToken = default) + { + return _keycloakClient.GetUsersAsync(_keycloakOptions.RealmName, search: search, username: username, + email: email, cancellationToken: cancellationToken); + } + + public Task GetUserAsync(string userId, CancellationToken cancellationToken = default) + { + return _keycloakClient.GetUserAsync(_keycloakOptions.RealmName, userId, cancellationToken: cancellationToken); + } + + public Task CreateUserAsync(User user, CancellationToken cancellationToken = default) + { + return _keycloakClient.CreateUserAsync(_keycloakOptions.RealmName, user, cancellationToken); + } + + public Task UpdateUserAsync(string userId, User user, CancellationToken cancellationToken = default) + { + return _keycloakClient.UpdateUserAsync(_keycloakOptions.RealmName, userId, user, cancellationToken); + } + + public Task DeleteUserAsync(string userId, CancellationToken cancellationToken = default) + { + return _keycloakClient.DeleteUserAsync(_keycloakOptions.RealmName, userId, cancellationToken); + } + + public Task> GetRolesAsync(CancellationToken cancellationToken = default) + { + return _keycloakClient.GetRolesAsync(_keycloakOptions.RealmName, cancellationToken: cancellationToken); + } + + public Task AddRolesToUserAsync(string userId, IEnumerable roles, + CancellationToken cancellationToken = default) + { + return _keycloakClient.AddRealmRoleMappingsToUserAsync(_keycloakOptions.RealmName, userId, roles, + cancellationToken); + } +} \ No newline at end of file From 2bd2e6d5dd0e2d614f765a202ac495f8d091bfe7 Mon Sep 17 00:00:00 2001 From: Galip Tolga Erdem Date: Mon, 24 Jul 2023 17:19:59 -0400 Subject: [PATCH 3/9] Added backgroundjobs for keycloak sync --- .../{ => User}/KeycloakUserCreationJob.cs | 46 +++++++++++-------- .../KeycloakUserDeletionJob.cs} | 26 ++++------- .../{ => User}/KeycloakUserUpdatingJob.cs | 25 +++++----- 3 files changed, 48 insertions(+), 49 deletions(-) rename services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/{ => User}/KeycloakUserCreationJob.cs (52%) rename services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/{IdentityUserDeletionJob.cs => User/KeycloakUserDeletionJob.cs} (56%) rename services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/{ => User}/KeycloakUserUpdatingJob.cs (67%) diff --git a/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/KeycloakUserCreationJob.cs b/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/User/KeycloakUserCreationJob.cs similarity index 52% rename from services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/KeycloakUserCreationJob.cs rename to services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/User/KeycloakUserCreationJob.cs index 1da1cbca..0727351e 100644 --- a/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/KeycloakUserCreationJob.cs +++ b/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/User/KeycloakUserCreationJob.cs @@ -1,43 +1,36 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; -using Keycloak.Net; -using Keycloak.Net.Models.Users; +using EShopOnAbp.IdentityService.Keycloak; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; using Volo.Abp.BackgroundJobs; using Volo.Abp.DependencyInjection; +using Keycloak.Net.Models.Users; -namespace EShopOnAbp.IdentityService.BackgroundJobs; +namespace EShopOnAbp.IdentityService.BackgroundJobs.User; public class KeycloakUserCreationJob : AsyncBackgroundJob, ITransientDependency { - private readonly KeycloakClient _keycloakClient; - private readonly KeycloakClientOptions _keycloakOptions; - private readonly ILogger _logger; + private readonly KeycloakService _keycloakService; + private readonly ILogger _logger; - public KeycloakUserCreationJob(IOptions keycloakOptions, + public KeycloakUserCreationJob(KeycloakService keycloakService, ILogger logger) { _logger = logger; - _keycloakOptions = keycloakOptions.Value; - - _keycloakClient = new KeycloakClient( - _keycloakOptions.Url, - _keycloakOptions.AdminUserName, - _keycloakOptions.AdminPassword - ); + _keycloakService = keycloakService; } public override async Task ExecuteAsync(IdentityUserCreationArgs args) { - var keycloakUser = new User + var keycloakUser = new global::Keycloak.Net.Models.Users.User() { Email = args.Email, UserName = args.UserName, FirstName = args.Name, LastName = args.Surname, - Enabled = true, + Enabled = args.IsActive, Credentials = new List() { new() { Type = "password", Value = args.Password } @@ -46,9 +39,14 @@ public class KeycloakUserCreationJob : AsyncBackgroundJob roleNames.Contains(q.Name)); + + await _keycloakService.AddRolesToUserAsync(user.Id, roles); + _logger.LogInformation($"Keycloak roles:{roleNames} has been added to user with the username:{userName}."); + } } public class IdentityUserCreationArgs @@ -67,4 +75,6 @@ public class IdentityUserCreationArgs public string Name { get; set; } public string Surname { get; set; } public string Password { get; set; } + public bool IsActive { get; set; } + public string[] RoleNames { get; set; } } \ No newline at end of file diff --git a/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/IdentityUserDeletionJob.cs b/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/User/KeycloakUserDeletionJob.cs similarity index 56% rename from services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/IdentityUserDeletionJob.cs rename to services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/User/KeycloakUserDeletionJob.cs index 21206599..9a7c2517 100644 --- a/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/IdentityUserDeletionJob.cs +++ b/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/User/KeycloakUserDeletionJob.cs @@ -1,39 +1,31 @@ using System; using System.Linq; using System.Threading.Tasks; -using Keycloak.Net; +using EShopOnAbp.IdentityService.Keycloak; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; using Volo.Abp; using Volo.Abp.BackgroundJobs; using Volo.Abp.DependencyInjection; -namespace EShopOnAbp.IdentityService.BackgroundJobs; +namespace EShopOnAbp.IdentityService.BackgroundJobs.User; -public class IdentityUserDeletionJob : AsyncBackgroundJob, ITransientDependency +public class KeycloakUserDeletionJob : AsyncBackgroundJob, ITransientDependency { - private readonly KeycloakClient _keycloakClient; - private readonly KeycloakClientOptions _keycloakOptions; - private readonly ILogger _logger; + private readonly KeycloakService _keycloakService; + private readonly ILogger _logger; - public IdentityUserDeletionJob(IOptions keycloakOptions, + public KeycloakUserDeletionJob(KeycloakService keycloakService, ILogger logger) { + _keycloakService = keycloakService; _logger = logger; - _keycloakOptions = keycloakOptions.Value; - - _keycloakClient = new KeycloakClient( - _keycloakOptions.Url, - _keycloakOptions.AdminUserName, - _keycloakOptions.AdminPassword - ); } public override async Task ExecuteAsync(IdentityUserDeletionArgs args) { try { - var keycloakUser = (await _keycloakClient.GetUsersAsync(_keycloakOptions.RealmName, username: args.UserName)) + var keycloakUser = (await _keycloakService.GetUsersAsync(username: args.UserName)) .First(); if (keycloakUser == null) { @@ -41,7 +33,7 @@ public class IdentityUserDeletionJob : AsyncBackgroundJob, ITransientDependency { - private readonly KeycloakClient _keycloakClient; - private readonly KeycloakClientOptions _keycloakOptions; + private readonly KeycloakService _keycloakService; private readonly ILogger _logger; - public KeycloakUserUpdatingJob(IOptions keycloakOptions, ILogger logger) + public KeycloakUserUpdatingJob(KeycloakService keycloakService, ILogger logger) { + _keycloakService = keycloakService; _logger = logger; - _keycloakOptions = keycloakOptions.Value; - - _keycloakClient = new KeycloakClient( - _keycloakOptions.Url, - _keycloakOptions.AdminUserName, - _keycloakOptions.AdminPassword - ); } public override async Task ExecuteAsync(IdentityUserUpdatingArgs args) { try { - var keycloakUser = (await _keycloakClient.GetUsersAsync(_keycloakOptions.RealmName, username: args.UserName)) + var keycloakUser = (await _keycloakService.GetUsersAsync( username: args.UserName)) .First(); if (keycloakUser == null) { @@ -45,9 +38,11 @@ public class KeycloakUserUpdatingJob : AsyncBackgroundJob Date: Mon, 24 Jul 2023 18:06:55 -0400 Subject: [PATCH 4/9] updated custom identity user app service --- .../User/KeycloakUserCreationJob.cs | 31 ++++++++++++++----- .../User/KeycloakUserDeletionJob.cs | 9 +++++- .../User/KeycloakUserUpdatingJob.cs | 23 ++++++++------ .../EShopIdentityUserAppService.cs | 31 ++++++++----------- 4 files changed, 58 insertions(+), 36 deletions(-) diff --git a/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/User/KeycloakUserCreationJob.cs b/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/User/KeycloakUserCreationJob.cs index 0727351e..c7671a0a 100644 --- a/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/User/KeycloakUserCreationJob.cs +++ b/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/User/KeycloakUserCreationJob.cs @@ -7,6 +7,7 @@ using Microsoft.Extensions.Logging; using Volo.Abp.BackgroundJobs; using Volo.Abp.DependencyInjection; using Keycloak.Net.Models.Users; +using Volo.Abp.Identity; namespace EShopOnAbp.IdentityService.BackgroundJobs.User; @@ -31,6 +32,7 @@ public class KeycloakUserCreationJob : AsyncBackgroundJob() { new() { Type = "password", Value = args.Password } @@ -70,11 +72,26 @@ public class KeycloakUserCreationJob : AsyncBackgroundJob identityOptions, IBackgroundJobManager backgroundJobManager) : base(userManager, + IOptions identityOptions, + IBackgroundJobManager backgroundJobManager) : base(userManager, userRepository, roleRepository, identityOptions) { + _userManager = userManager; _backgroundJobManager = backgroundJobManager; - _userRepository = userRepository; } public override async Task CreateAsync(IdentityUserCreateDto input) { var createdUser = await base.CreateAsync(input); - await _backgroundJobManager.EnqueueAsync(new IdentityUserCreationArgs - { - Email = createdUser.Email, - UserName = createdUser.UserName, - Name = createdUser.Name, - Surname = createdUser.Surname, - Password = input.Password - }); + await _backgroundJobManager.EnqueueAsync(new IdentityUserCreationArgs(input)); return createdUser; } @@ -52,7 +46,9 @@ public class EShopIdentityUserAppService : IdentityUserAppService UserName = updatedUser.UserName, Name = updatedUser.Name, Surname = updatedUser.Surname, - EmailConfirmed = updatedUser.EmailConfirmed + EmailConfirmed = updatedUser.EmailConfirmed, + IsActive = input.IsActive, + RoleNames = input.RoleNames }); return updatedUser; @@ -60,12 +56,11 @@ public class EShopIdentityUserAppService : IdentityUserAppService public override async Task DeleteAsync(Guid id) { - var user = await _userRepository.FindAsync(id); + var user = await _userManager.FindByIdAsync(id.ToString()); await base.DeleteAsync(id); - - await _backgroundJobManager.EnqueueAsync(new IdentityUserDeletionArgs + if (user != null) { - UserName = user.UserName - }); + await _backgroundJobManager.EnqueueAsync(new IdentityUserDeletionArgs(user.UserName)); + } } } \ No newline at end of file From 4085d565ca77f282e399dd1ec65e7fca1d4adf11 Mon Sep 17 00:00:00 2001 From: Galip Tolga Erdem Date: Tue, 25 Jul 2023 18:58:52 -0400 Subject: [PATCH 5/9] Added Keycloak role jobs --- .../Roles/KeycloakRoleCreationJob.cs | 42 +++++++++++++++ .../Roles/KeycloakRoleDeletionJob.cs | 42 +++++++++++++++ .../Roles/KeycloakRoleUpdatingJob.cs | 45 ++++++++++++++++ .../Identity/EShopIdentityRoleAppService.cs | 51 +++++++++++++++++++ .../EShopIdentityUserAppService.cs | 4 +- .../Keycloak/KeycloakService.cs | 19 +++++++ .../KeycloakDataSeeder.cs | 2 +- 7 files changed, 202 insertions(+), 3 deletions(-) create mode 100644 services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/Roles/KeycloakRoleCreationJob.cs create mode 100644 services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/Roles/KeycloakRoleDeletionJob.cs create mode 100644 services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/Roles/KeycloakRoleUpdatingJob.cs create mode 100644 services/identity/src/EShopOnAbp.IdentityService.Application/Identity/EShopIdentityRoleAppService.cs rename services/identity/src/EShopOnAbp.IdentityService.Application/{ => Identity}/EShopIdentityUserAppService.cs (95%) diff --git a/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/Roles/KeycloakRoleCreationJob.cs b/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/Roles/KeycloakRoleCreationJob.cs new file mode 100644 index 00000000..096b0c61 --- /dev/null +++ b/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; +using Microsoft.Extensions.Logging; +using Volo.Abp.BackgroundJobs; +using Volo.Abp.DependencyInjection; + +namespace EShopOnAbp.IdentityService.BackgroundJobs.Roles; + +public class KeycloakRoleCreationJob : AsyncBackgroundJob, ITransientDependency +{ + private readonly KeycloakService _keycloakService; + private readonly ILogger _logger; + + public KeycloakRoleCreationJob(KeycloakService keycloakService, ILogger 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); \ No newline at end of file diff --git a/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/Roles/KeycloakRoleDeletionJob.cs b/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/Roles/KeycloakRoleDeletionJob.cs new file mode 100644 index 00000000..812bf290 --- /dev/null +++ b/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; +using Microsoft.Extensions.Logging; +using Volo.Abp.BackgroundJobs; +using Volo.Abp.DependencyInjection; + +namespace EShopOnAbp.IdentityService.BackgroundJobs.Roles; + +public class KeycloakRoleDeletionJob : AsyncBackgroundJob, ITransientDependency +{ + private readonly KeycloakService _keycloakService; + private readonly ILogger _logger; + + public KeycloakRoleDeletionJob(KeycloakService keycloakService, ILogger 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); \ No newline at end of file diff --git a/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/Roles/KeycloakRoleUpdatingJob.cs b/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/Roles/KeycloakRoleUpdatingJob.cs new file mode 100644 index 00000000..b7d0f95c --- /dev/null +++ b/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/Roles/KeycloakRoleUpdatingJob.cs @@ -0,0 +1,45 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using EShopOnAbp.IdentityService.Keycloak; +using Microsoft.Extensions.Logging; +using Volo.Abp.BackgroundJobs; +using Volo.Abp.DependencyInjection; + +namespace EShopOnAbp.IdentityService.BackgroundJobs.Roles; + +public class KeycloakRoleUpdatingJob : AsyncBackgroundJob, ITransientDependency +{ + private readonly KeycloakService _keycloakService; + private readonly ILogger _logger; + + public KeycloakRoleUpdatingJob(KeycloakService keycloakService, ILogger logger) + { + _keycloakService = keycloakService; + _logger = logger; + } + + public override async Task ExecuteAsync(IdentityRoleUpdatingArgs args) + { + try + { + var existingRole = (await _keycloakService.GetRolesAsync()).FirstOrDefault(q => q.Name == args.Name); + if (existingRole == null) + { + _logger.LogWarning($"Role with the name:{args.Name} couldn't be found to update!"); + return; + } + + existingRole.Name = args.Name; + + await _keycloakService.UpdateRoleAsync(existingRole.Id, existingRole); + } + catch (Exception e) + { + _logger.LogWarning($"Could not delete the role with the name:{args.Name} from Keycloak server!"); + throw; + } + } +} + +public record IdentityRoleUpdatingArgs(string Name); \ No newline at end of file diff --git a/services/identity/src/EShopOnAbp.IdentityService.Application/Identity/EShopIdentityRoleAppService.cs b/services/identity/src/EShopOnAbp.IdentityService.Application/Identity/EShopIdentityRoleAppService.cs new file mode 100644 index 00000000..20d7c5e1 --- /dev/null +++ b/services/identity/src/EShopOnAbp.IdentityService.Application/Identity/EShopIdentityRoleAppService.cs @@ -0,0 +1,51 @@ +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 CreateAsync(IdentityRoleCreateDto input) + { + var result = await base.CreateAsync(input); + await _backgroundJobManager.EnqueueAsync(new IdentityRoleCreationArgs(result.Name)); + + return result; + } + + public override async Task UpdateAsync(Guid id, IdentityRoleUpdateDto input) + { + var result = await base.UpdateAsync(id, input); + await _backgroundJobManager.EnqueueAsync(new IdentityRoleUpdatingArgs(result.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)); + } +} \ No newline at end of file diff --git a/services/identity/src/EShopOnAbp.IdentityService.Application/EShopIdentityUserAppService.cs b/services/identity/src/EShopOnAbp.IdentityService.Application/Identity/EShopIdentityUserAppService.cs similarity index 95% rename from services/identity/src/EShopOnAbp.IdentityService.Application/EShopIdentityUserAppService.cs rename to services/identity/src/EShopOnAbp.IdentityService.Application/Identity/EShopIdentityUserAppService.cs index 39ac34cc..8a370a23 100644 --- a/services/identity/src/EShopOnAbp.IdentityService.Application/EShopIdentityUserAppService.cs +++ b/services/identity/src/EShopOnAbp.IdentityService.Application/Identity/EShopIdentityUserAppService.cs @@ -1,13 +1,13 @@ using System; using System.Threading.Tasks; -using EShopOnAbp.IdentityService.BackgroundJobs.User; +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; +namespace EShopOnAbp.IdentityService.Identity; [ExposeServices(typeof(IdentityUserAppService), typeof(IIdentityUserAppService))] public class EShopIdentityUserAppService : IdentityUserAppService diff --git a/services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/KeycloakService.cs b/services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/KeycloakService.cs index 19da1314..175d5d7d 100644 --- a/services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/KeycloakService.cs +++ b/services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/KeycloakService.cs @@ -63,4 +63,23 @@ public class KeycloakService : ITransientDependency return _keycloakClient.AddRealmRoleMappingsToUserAsync(_keycloakOptions.RealmName, userId, roles, cancellationToken); } + + public Task CreateRoleAsync(string name, CancellationToken cancellationToken = default) + { + Role role = new Role + { + Name = name + }; + return _keycloakClient.CreateRoleAsync(_keycloakOptions.RealmName, role, cancellationToken); + } + + public Task DeleteRoleByIdAsync(string id, CancellationToken cancellationToken = default) + { + return _keycloakClient.DeleteRoleByIdAsync(_keycloakOptions.RealmName, id, cancellationToken); + } + + public Task UpdateRoleAsync(string id, Role role, CancellationToken cancellationToken = default) + { + return _keycloakClient.UpdateRoleByIdAsync(_keycloakOptions.RealmName, id, role, cancellationToken); + } } \ No newline at end of file diff --git a/shared/EShopOnAbp.Keycloak.DbMigrator/KeycloakDataSeeder.cs b/shared/EShopOnAbp.Keycloak.DbMigrator/KeycloakDataSeeder.cs index 73259647..3f297757 100644 --- a/shared/EShopOnAbp.Keycloak.DbMigrator/KeycloakDataSeeder.cs +++ b/shared/EShopOnAbp.Keycloak.DbMigrator/KeycloakDataSeeder.cs @@ -39,7 +39,7 @@ public class KeyCloakDataSeeder : IDataSeedContributor, ITransientDependency { await UpdateRealmSettingsAsync(); await UpdateAdminUserAsync(); - await CreateRoleMapperAsync(); + await CreateRoleMapperAsync(); // roles scope await CreateClientScopesAsync(); await CreateClientsAsync(); } From 63ef4ea5c0a88e534ce07425a0762fe9023e1611 Mon Sep 17 00:00:00 2001 From: Galip Tolga Erdem Date: Tue, 25 Jul 2023 18:59:12 -0400 Subject: [PATCH 6/9] moved keycloak user jobs --- .../{User => Users}/KeycloakUserCreationJob.cs | 7 ++++--- .../{User => Users}/KeycloakUserDeletionJob.cs | 7 +++++-- .../{User => Users}/KeycloakUserUpdatingJob.cs | 5 +---- 3 files changed, 10 insertions(+), 9 deletions(-) rename services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/{User => Users}/KeycloakUserCreationJob.cs (95%) rename services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/{User => Users}/KeycloakUserDeletionJob.cs (93%) rename services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/{User => Users}/KeycloakUserUpdatingJob.cs (94%) diff --git a/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/User/KeycloakUserCreationJob.cs b/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/Users/KeycloakUserCreationJob.cs similarity index 95% rename from services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/User/KeycloakUserCreationJob.cs rename to services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/Users/KeycloakUserCreationJob.cs index c7671a0a..44f2e207 100644 --- a/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/User/KeycloakUserCreationJob.cs +++ b/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/Users/KeycloakUserCreationJob.cs @@ -3,13 +3,13 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using EShopOnAbp.IdentityService.Keycloak; +using Keycloak.Net.Models.Users; using Microsoft.Extensions.Logging; using Volo.Abp.BackgroundJobs; using Volo.Abp.DependencyInjection; -using Keycloak.Net.Models.Users; using Volo.Abp.Identity; -namespace EShopOnAbp.IdentityService.BackgroundJobs.User; +namespace EShopOnAbp.IdentityService.BackgroundJobs.Users; public class KeycloakUserCreationJob : AsyncBackgroundJob, ITransientDependency { @@ -25,7 +25,7 @@ public class KeycloakUserCreationJob : AsyncBackgroundJob, ITransientDependency { @@ -50,7 +50,10 @@ public class IdentityUserDeletionArgs { public string UserName { get; init; } - public IdentityUserDeletionArgs() { } + public IdentityUserDeletionArgs() + { + + } public IdentityUserDeletionArgs(string userName) { diff --git a/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/User/KeycloakUserUpdatingJob.cs b/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/Users/KeycloakUserUpdatingJob.cs similarity index 94% rename from services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/User/KeycloakUserUpdatingJob.cs rename to services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/Users/KeycloakUserUpdatingJob.cs index 2e52f8d7..8bda0faf 100644 --- a/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/User/KeycloakUserUpdatingJob.cs +++ b/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/Users/KeycloakUserUpdatingJob.cs @@ -2,15 +2,12 @@ using System.Linq; using System.Threading.Tasks; using EShopOnAbp.IdentityService.Keycloak; -using Keycloak.Net; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; using Volo.Abp; using Volo.Abp.BackgroundJobs; using Volo.Abp.DependencyInjection; -using Volo.Abp.Identity; -namespace EShopOnAbp.IdentityService.BackgroundJobs.User; +namespace EShopOnAbp.IdentityService.BackgroundJobs.Users; public class KeycloakUserUpdatingJob : AsyncBackgroundJob, ITransientDependency { From 2600b6bf0808ba212615b5a25ab7ed228113bf6d Mon Sep 17 00:00:00 2001 From: Galip Tolga Erdem Date: Wed, 26 Jul 2023 20:24:42 -0400 Subject: [PATCH 7/9] Added caching --- .env.example | 2 - .../Roles/KeycloakRoleCreationJob.cs | 6 +- .../Roles/KeycloakRoleDeletionJob.cs | 6 +- .../Roles/KeycloakRoleUpdatingJob.cs | 28 ++- .../Users/KeycloakUserCreationJob.cs | 24 ++- .../Users/KeycloakUserDeletionJob.cs | 11 +- .../Users/KeycloakUserUpdatingJob.cs | 95 +++++++-- ...opOnAbpIdentityServiceAutoMapperProfile.cs | 21 ++ .../Roles/KeycloakRolesEventHandler.cs | 47 +++++ .../Identity/EShopIdentityRoleAppService.cs | 7 +- .../Identity/EShopIdentityUserAppService.cs | 47 +++-- .../IdentityServiceApplicationModule.cs | 1 + .../Keycloak/KeycloakService.cs | 85 -------- .../Keycloak/Service/CachedKeycloakRole.cs | 23 +++ .../Keycloak/Service/CachedKeycloakService.cs | 127 ++++++++++++ .../Keycloak/Service/CachedKeycloakUser.cs | 75 ++++++++ .../Keycloak/Service/IKeycloakService.cs | 37 ++++ .../{ => Service}/KeycloakClientOptions.cs | 2 +- .../Keycloak/Service/KeycloakService.cs | 182 ++++++++++++++++++ .../Service/KeycloakServiceExtensions.cs | 31 +++ 20 files changed, 708 insertions(+), 149 deletions(-) delete mode 100644 .env.example create mode 100644 services/identity/src/EShopOnAbp.IdentityService.Application/EShopOnAbpIdentityServiceAutoMapperProfile.cs create mode 100644 services/identity/src/EShopOnAbp.IdentityService.Application/EventHandlers/Roles/KeycloakRolesEventHandler.cs delete mode 100644 services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/KeycloakService.cs create mode 100644 services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/Service/CachedKeycloakRole.cs create mode 100644 services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/Service/CachedKeycloakService.cs create mode 100644 services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/Service/CachedKeycloakUser.cs create mode 100644 services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/Service/IKeycloakService.cs rename services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/{ => Service}/KeycloakClientOptions.cs (78%) create mode 100644 services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/Service/KeycloakService.cs create mode 100644 services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/Service/KeycloakServiceExtensions.cs diff --git a/.env.example b/.env.example deleted file mode 100644 index 4f042dca..00000000 --- a/.env.example +++ /dev/null @@ -1,2 +0,0 @@ -#Payment__PayPal__ClientId=PAYPAL_CLIENT_ID -#Payment__PayPal__Secret=PAYPAL_SECRET diff --git a/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/Roles/KeycloakRoleCreationJob.cs b/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/Roles/KeycloakRoleCreationJob.cs index 096b0c61..58bdacb3 100644 --- a/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/Roles/KeycloakRoleCreationJob.cs +++ b/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/Roles/KeycloakRoleCreationJob.cs @@ -1,7 +1,7 @@ using System; using System.Linq; using System.Threading.Tasks; -using EShopOnAbp.IdentityService.Keycloak; +using EShopOnAbp.IdentityService.Keycloak.Service; using Microsoft.Extensions.Logging; using Volo.Abp.BackgroundJobs; using Volo.Abp.DependencyInjection; @@ -10,10 +10,10 @@ namespace EShopOnAbp.IdentityService.BackgroundJobs.Roles; public class KeycloakRoleCreationJob : AsyncBackgroundJob, ITransientDependency { - private readonly KeycloakService _keycloakService; + private readonly IKeycloakService _keycloakService; private readonly ILogger _logger; - public KeycloakRoleCreationJob(KeycloakService keycloakService, ILogger logger) + public KeycloakRoleCreationJob(IKeycloakService keycloakService, ILogger logger) { _keycloakService = keycloakService; _logger = logger; diff --git a/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/Roles/KeycloakRoleDeletionJob.cs b/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/Roles/KeycloakRoleDeletionJob.cs index 812bf290..cba36100 100644 --- a/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/Roles/KeycloakRoleDeletionJob.cs +++ b/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/Roles/KeycloakRoleDeletionJob.cs @@ -1,7 +1,7 @@ using System; using System.Linq; using System.Threading.Tasks; -using EShopOnAbp.IdentityService.Keycloak; +using EShopOnAbp.IdentityService.Keycloak.Service; using Microsoft.Extensions.Logging; using Volo.Abp.BackgroundJobs; using Volo.Abp.DependencyInjection; @@ -10,10 +10,10 @@ namespace EShopOnAbp.IdentityService.BackgroundJobs.Roles; public class KeycloakRoleDeletionJob : AsyncBackgroundJob, ITransientDependency { - private readonly KeycloakService _keycloakService; + private readonly IKeycloakService _keycloakService; private readonly ILogger _logger; - public KeycloakRoleDeletionJob(KeycloakService keycloakService, ILogger logger) + public KeycloakRoleDeletionJob(IKeycloakService keycloakService, ILogger logger) { _keycloakService = keycloakService; _logger = logger; diff --git a/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/Roles/KeycloakRoleUpdatingJob.cs b/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/Roles/KeycloakRoleUpdatingJob.cs index b7d0f95c..3d4050a9 100644 --- a/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/Roles/KeycloakRoleUpdatingJob.cs +++ b/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/Roles/KeycloakRoleUpdatingJob.cs @@ -1,45 +1,55 @@ using System; using System.Linq; using System.Threading.Tasks; -using EShopOnAbp.IdentityService.Keycloak; +using EShopOnAbp.IdentityService.Keycloak.Service; +using Keycloak.Net.Models.Roles; using Microsoft.Extensions.Logging; using Volo.Abp.BackgroundJobs; using Volo.Abp.DependencyInjection; +using Volo.Abp.ObjectMapping; namespace EShopOnAbp.IdentityService.BackgroundJobs.Roles; public class KeycloakRoleUpdatingJob : AsyncBackgroundJob, ITransientDependency { - private readonly KeycloakService _keycloakService; + private readonly IKeycloakService _keycloakService; private readonly ILogger _logger; + private readonly IObjectMapper _objectMapper; - public KeycloakRoleUpdatingJob(KeycloakService keycloakService, ILogger logger) + public KeycloakRoleUpdatingJob(IKeycloakService keycloakService, ILogger logger, + IObjectMapper objectMapper) { _keycloakService = keycloakService; _logger = logger; + _objectMapper = objectMapper; } public override async Task ExecuteAsync(IdentityRoleUpdatingArgs args) { try { - var existingRole = (await _keycloakService.GetRolesAsync()).FirstOrDefault(q => q.Name == args.Name); + var existingRole = (await _keycloakService.GetRolesAsync()).FirstOrDefault(q => q.Name == args.OldName); if (existingRole == null) { - _logger.LogWarning($"Role with the name:{args.Name} couldn't be found to update!"); + _logger.LogWarning($"Role with the name:{args.OldName} couldn't be found to update!"); return; } - existingRole.Name = args.Name; + if (args.OldName != args.NewName) + { + existingRole.Name = args.NewName; - await _keycloakService.UpdateRoleAsync(existingRole.Id, existingRole); + await _keycloakService.UpdateRoleAsync(existingRole.Id, + _objectMapper.Map(existingRole) + ); + } } catch (Exception e) { - _logger.LogWarning($"Could not delete the role with the name:{args.Name} from Keycloak server!"); + _logger.LogWarning($"Could not update the role with the name:{args.OldName} from Keycloak server!"); throw; } } } -public record IdentityRoleUpdatingArgs(string Name); \ No newline at end of file +public record IdentityRoleUpdatingArgs(string OldName, string NewName); \ No newline at end of file diff --git a/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/Users/KeycloakUserCreationJob.cs b/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/Users/KeycloakUserCreationJob.cs index 44f2e207..7f3b4400 100644 --- a/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/Users/KeycloakUserCreationJob.cs +++ b/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/Users/KeycloakUserCreationJob.cs @@ -2,24 +2,28 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using EShopOnAbp.IdentityService.Keycloak; +using EShopOnAbp.IdentityService.Keycloak.Service; +using Keycloak.Net.Models.Roles; using Keycloak.Net.Models.Users; using Microsoft.Extensions.Logging; using Volo.Abp.BackgroundJobs; using Volo.Abp.DependencyInjection; using Volo.Abp.Identity; +using Volo.Abp.ObjectMapping; namespace EShopOnAbp.IdentityService.BackgroundJobs.Users; public class KeycloakUserCreationJob : AsyncBackgroundJob, ITransientDependency { - private readonly KeycloakService _keycloakService; + private readonly IKeycloakService _keycloakService; private readonly ILogger _logger; + private readonly IObjectMapper _objectMapper; - public KeycloakUserCreationJob(KeycloakService keycloakService, - ILogger logger) + public KeycloakUserCreationJob(IKeycloakService keycloakService, + ILogger logger, IObjectMapper objectMapper) { _logger = logger; + _objectMapper = objectMapper; _keycloakService = keycloakService; } @@ -32,7 +36,6 @@ public class KeycloakUserCreationJob : AsyncBackgroundJob() { new() { Type = "password", Value = args.Password } @@ -61,11 +64,14 @@ public class KeycloakUserCreationJob : AsyncBackgroundJobq.UserName == userName); var allTheRoles = await _keycloakService.GetRolesAsync(); - var roles = allTheRoles.Where(q => roleNames.Contains(q.Name)); + var roles = allTheRoles.Where(q => roleNames.Contains(q.Name)).ToList(); - await _keycloakService.AddRolesToUserAsync(user.Id, roles); + await _keycloakService.AddRealmRolesToUserAsync( + user.Id, + _objectMapper.Map,List>(roles) + ); _logger.LogInformation($"Keycloak roles:{roleNames} has been added to user with the username:{userName}."); } } @@ -80,7 +86,7 @@ public class IdentityUserCreationArgs public bool IsActive { get; init; } public string[] RoleNames { get; init; } - public IdentityUserCreationArgs() + public IdentityUserCreationArgs() // For deserialization { } diff --git a/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/Users/KeycloakUserDeletionJob.cs b/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/Users/KeycloakUserDeletionJob.cs index 0dbdcb40..1c7d7c57 100644 --- a/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/Users/KeycloakUserDeletionJob.cs +++ b/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/Users/KeycloakUserDeletionJob.cs @@ -1,7 +1,7 @@ using System; using System.Linq; using System.Threading.Tasks; -using EShopOnAbp.IdentityService.Keycloak; +using EShopOnAbp.IdentityService.Keycloak.Service; using Microsoft.Extensions.Logging; using Volo.Abp; using Volo.Abp.BackgroundJobs; @@ -11,10 +11,10 @@ namespace EShopOnAbp.IdentityService.BackgroundJobs.Users; public class KeycloakUserDeletionJob : AsyncBackgroundJob, ITransientDependency { - private readonly KeycloakService _keycloakService; + private readonly IKeycloakService _keycloakService; private readonly ILogger _logger; - public KeycloakUserDeletionJob(KeycloakService keycloakService, + public KeycloakUserDeletionJob(IKeycloakService keycloakService, ILogger logger) { _keycloakService = keycloakService; @@ -25,8 +25,8 @@ public class KeycloakUserDeletionJob : AsyncBackgroundJob q.UserName == args.UserName); if (keycloakUser == null) { _logger.LogError($"Keycloak user could not be found to delete! Username:{args.UserName}"); @@ -52,7 +52,6 @@ public class IdentityUserDeletionArgs public IdentityUserDeletionArgs() { - } public IdentityUserDeletionArgs(string userName) diff --git a/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/Users/KeycloakUserUpdatingJob.cs b/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/Users/KeycloakUserUpdatingJob.cs index 8bda0faf..5308ed5e 100644 --- a/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/Users/KeycloakUserUpdatingJob.cs +++ b/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/Users/KeycloakUserUpdatingJob.cs @@ -1,49 +1,70 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using EShopOnAbp.IdentityService.Keycloak; +using EShopOnAbp.IdentityService.Keycloak.Service; +using Keycloak.Net.Models.Users; using Microsoft.Extensions.Logging; using Volo.Abp; using Volo.Abp.BackgroundJobs; using Volo.Abp.DependencyInjection; +using Volo.Abp.ObjectMapping; namespace EShopOnAbp.IdentityService.BackgroundJobs.Users; public class KeycloakUserUpdatingJob : AsyncBackgroundJob, ITransientDependency { - private readonly KeycloakService _keycloakService; + private readonly IKeycloakService _keycloakService; private readonly ILogger _logger; + private readonly IObjectMapper _objectMapper; - public KeycloakUserUpdatingJob(KeycloakService keycloakService, ILogger logger) + public KeycloakUserUpdatingJob(IKeycloakService keycloakService, ILogger logger, IObjectMapper objectMapper) { _keycloakService = keycloakService; _logger = logger; + _objectMapper = objectMapper; } public override async Task ExecuteAsync(IdentityUserUpdatingArgs args) { try { - var keycloakUser = (await _keycloakService.GetUsersAsync(username: args.UserName)) - .First(); + var keycloakUser = (await _keycloakService.GetUsersAsync()) + .FirstOrDefault(q => q.UserName == args.UserName); if (keycloakUser == null) { _logger.LogError($"Keycloak user could not be found to update! Username:{args.UserName}"); throw new UserFriendlyException($"Keycloak user with the username:{args.UserName} could not be found!"); } - keycloakUser.UserName = args.UserName; - keycloakUser.Email = args.Email; - keycloakUser.FirstName = args.Name; - keycloakUser.LastName = args.Surname; - keycloakUser.Enabled = args.IsActive; - keycloakUser.EmailVerified = args.EmailConfirmed; + IEnumerable differentFields = args.GetDifferentFields().ToList(); + foreach (var fieldChange in differentFields) + { + if (fieldChange.FieldName == "Email") + keycloakUser.Email = fieldChange.NewValue.ToString(); + if (fieldChange.FieldName == "UserName") + keycloakUser.UserName = fieldChange.NewValue.ToString(); + if (fieldChange.FieldName == "Name") + keycloakUser.FirstName = fieldChange.NewValue.ToString(); + if (fieldChange.FieldName == "Surname") + keycloakUser.LastName = fieldChange.NewValue.ToString(); + if (fieldChange.FieldName == "IsActive") + keycloakUser.Enabled = (bool)fieldChange.NewValue; + if (fieldChange.FieldName == "RoleNames") + keycloakUser.RealmRoles = (string[])fieldChange.NewValue; + } - var result = - await _keycloakService.UpdateUserAsync(keycloakUser.Id, keycloakUser); - if (result) + if (differentFields.Count() != 0) { - _logger.LogInformation($"Keycloak user with the username:{args.UserName} has been updated."); + var mappedUser = _objectMapper.Map(keycloakUser); + var result = await _keycloakService.UpdateUserAsync( + keycloakUser.Id, + mappedUser + ); + if (result) + { + _logger.LogInformation($"Keycloak user with the username:{args.UserName} has been updated."); + } } } catch (Exception e) @@ -58,10 +79,52 @@ public class KeycloakUserUpdatingJob : AsyncBackgroundJob GetDifferentFields() + { + List fieldChanges = new List(); + + if ((Email, OldEmail) is not (null, null) && Email != OldEmail) + fieldChanges.Add(new FieldChange { FieldName = nameof(Email), NewValue = Email, OldValue = OldEmail }); + + if ((UserName, OldUserName) is not (null, null) && UserName != OldUserName) + fieldChanges.Add(new FieldChange + { FieldName = nameof(UserName), NewValue = UserName, OldValue = OldUserName }); + + if ((Name, OldName) is not (null, null) && Name != OldName) + fieldChanges.Add(new FieldChange { FieldName = nameof(Name), NewValue = Name, OldValue = OldName }); + + if ((Surname, OldSurname) is not (null, null) && Surname != OldSurname) + fieldChanges.Add(new FieldChange + { FieldName = nameof(Surname), NewValue = Surname, OldValue = OldSurname }); + + if (IsActive != OldIsActive) + fieldChanges.Add(new FieldChange + { FieldName = nameof(IsActive), NewValue = IsActive, OldValue = OldIsActive }); + + if (!Enumerable.SequenceEqual(RoleNames ?? Enumerable.Empty(), + OldRoleNames ?? Enumerable.Empty())) + fieldChanges.Add(new FieldChange + { FieldName = nameof(RoleNames), NewValue = RoleNames, OldValue = OldRoleNames }); + + return fieldChanges; + } + + public class FieldChange + { + public string FieldName { get; set; } + public object NewValue { get; set; } + public object OldValue { get; set; } + } } \ No newline at end of file diff --git a/services/identity/src/EShopOnAbp.IdentityService.Application/EShopOnAbpIdentityServiceAutoMapperProfile.cs b/services/identity/src/EShopOnAbp.IdentityService.Application/EShopOnAbpIdentityServiceAutoMapperProfile.cs new file mode 100644 index 00000000..3146db25 --- /dev/null +++ b/services/identity/src/EShopOnAbp.IdentityService.Application/EShopOnAbpIdentityServiceAutoMapperProfile.cs @@ -0,0 +1,21 @@ +using AutoMapper; +using EShopOnAbp.IdentityService.Keycloak.Service; +using Keycloak.Net.Models.Roles; +using Keycloak.Net.Models.Users; + +namespace EShopOnAbp.IdentityService; + +public class EShopOnAbpIdentityServiceAutoMapperProfile : Profile +{ + public EShopOnAbpIdentityServiceAutoMapperProfile() + { + CreateMap().ReverseMap(); + CreateMap().ReverseMap(); + CreateMap().ReverseMap(); + CreateMap().ReverseMap(); + CreateMap(); + + CreateMap().ReverseMap(); + CreateMap().ReverseMap(); + } +} \ No newline at end of file diff --git a/services/identity/src/EShopOnAbp.IdentityService.Application/EventHandlers/Roles/KeycloakRolesEventHandler.cs b/services/identity/src/EShopOnAbp.IdentityService.Application/EventHandlers/Roles/KeycloakRolesEventHandler.cs new file mode 100644 index 00000000..f5ffe8e4 --- /dev/null +++ b/services/identity/src/EShopOnAbp.IdentityService.Application/EventHandlers/Roles/KeycloakRolesEventHandler.cs @@ -0,0 +1,47 @@ +// using System; +// using System.Linq; +// using System.Threading.Tasks; +// using EShopOnAbp.IdentityService.Keycloak; +// using Microsoft.Extensions.Logging; +// using Volo.Abp.DependencyInjection; +// using Volo.Abp.Domain.Entities.Events.Distributed; +// using Volo.Abp.EventBus.Distributed; +// using Volo.Abp.Identity; +// +// namespace EShopOnAbp.IdentityService.EventHandlers.Roles; +// +// public class KeycloakRolesEventHandler : IDistributedEventHandler>, +// ITransientDependency +// { +// private readonly KeycloakService _keycloakService; +// private readonly ILogger _logger; +// +// public KeycloakRolesEventHandler(KeycloakService keycloakService, ILogger logger) +// { +// _keycloakService = keycloakService; +// _logger = logger; +// } +// +// public async Task HandleEventAsync(EntityCreatedEto eventData) +// { +// try +// { +// var existingRole = (await _keycloakService.GetRolesAsync()).FirstOrDefault(q => q.Name == eventData.Entity.Name); +// if (existingRole != null) +// { +// return; +// } +// +// var isSuccess = await _keycloakService.CreateRoleAsync(eventData.Entity.Name); +// if (isSuccess) +// { +// _logger.LogInformation($"Role created:{eventData.Entity.Name}"); +// } +// } +// catch (Exception e) +// { +// _logger.LogError($"Keycloak role creation with the name:{eventData.Entity.Name} failed!"); +// throw; +// } +// } +// } \ No newline at end of file diff --git a/services/identity/src/EShopOnAbp.IdentityService.Application/Identity/EShopIdentityRoleAppService.cs b/services/identity/src/EShopOnAbp.IdentityService.Application/Identity/EShopIdentityRoleAppService.cs index 20d7c5e1..61a3840e 100644 --- a/services/identity/src/EShopOnAbp.IdentityService.Application/Identity/EShopIdentityRoleAppService.cs +++ b/services/identity/src/EShopOnAbp.IdentityService.Application/Identity/EShopIdentityRoleAppService.cs @@ -31,9 +31,12 @@ public class EShopIdentityRoleAppService : IdentityRoleAppService public override async Task UpdateAsync(Guid id, IdentityRoleUpdateDto input) { + var role = await _identityRoleManager.GetByIdAsync(id); + var existingRoleName = role.Name; var result = await base.UpdateAsync(id, input); - await _backgroundJobManager.EnqueueAsync(new IdentityRoleUpdatingArgs(result.Name)); - + + await _backgroundJobManager.EnqueueAsync(new IdentityRoleUpdatingArgs(existingRoleName, input.Name)); + return result; } diff --git a/services/identity/src/EShopOnAbp.IdentityService.Application/Identity/EShopIdentityUserAppService.cs b/services/identity/src/EShopOnAbp.IdentityService.Application/Identity/EShopIdentityUserAppService.cs index 8a370a23..2f10bfe7 100644 --- a/services/identity/src/EShopOnAbp.IdentityService.Application/Identity/EShopIdentityUserAppService.cs +++ b/services/identity/src/EShopOnAbp.IdentityService.Application/Identity/EShopIdentityUserAppService.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Threading.Tasks; using EShopOnAbp.IdentityService.BackgroundJobs.Users; using Microsoft.AspNetCore.Identity; @@ -12,8 +13,9 @@ namespace EShopOnAbp.IdentityService.Identity; [ExposeServices(typeof(IdentityUserAppService), typeof(IIdentityUserAppService))] public class EShopIdentityUserAppService : IdentityUserAppService { - private readonly IdentityUserManager _userManager; + private readonly IIdentityUserRepository _userRepository; private readonly IBackgroundJobManager _backgroundJobManager; + private readonly IIdentityRoleRepository _roleRepository; public EShopIdentityUserAppService( IdentityUserManager userManager, @@ -25,7 +27,8 @@ public class EShopIdentityUserAppService : IdentityUserAppService roleRepository, identityOptions) { - _userManager = userManager; + _userRepository = userRepository; + _roleRepository = roleRepository; _backgroundJobManager = backgroundJobManager; } @@ -39,28 +42,46 @@ public class EShopIdentityUserAppService : IdentityUserAppService public override async Task UpdateAsync(Guid id, IdentityUserUpdateDto input) { + var existingUser = await _userRepository.GetAsync(id); + var args = await CreateIdentityUserUpdatingArgsAsync(existingUser, input); var updatedUser = await base.UpdateAsync(id, input); - await _backgroundJobManager.EnqueueAsync(new IdentityUserUpdatingArgs - { - Email = updatedUser.Email, - UserName = updatedUser.UserName, - Name = updatedUser.Name, - Surname = updatedUser.Surname, - EmailConfirmed = updatedUser.EmailConfirmed, - IsActive = input.IsActive, - RoleNames = input.RoleNames - }); + await _backgroundJobManager.EnqueueAsync(args); return updatedUser; } public override async Task DeleteAsync(Guid id) { - var user = await _userManager.FindByIdAsync(id.ToString()); + var user = await _userRepository.FindAsync(id); await base.DeleteAsync(id); if (user != null) { await _backgroundJobManager.EnqueueAsync(new IdentityUserDeletionArgs(user.UserName)); } } + + private async Task CreateIdentityUserUpdatingArgsAsync(IdentityUser existingUser, + IdentityUserUpdateDto input) + { + var userRoles = existingUser.Roles.Select(q => q.RoleId).ToList(); + var roles = await _roleRepository.GetListAsync(); + + var args = new IdentityUserUpdatingArgs + { + Email = input.Email, + OldEmail = existingUser.Email, + UserName = input.UserName, + OldUserName = existingUser.UserName, + Name = input.Name, + OldName = existingUser.Name, + Surname = input.Surname, + OldSurname = existingUser.Surname, + IsActive = input.IsActive, + OldIsActive = existingUser.IsActive, + RoleNames = input.RoleNames, + OldRoleNames = roles.Where(q => userRoles.Contains(q.Id)).Select(q => q.Name).ToArray() + }; + + return args; + } } \ No newline at end of file diff --git a/services/identity/src/EShopOnAbp.IdentityService.Application/IdentityServiceApplicationModule.cs b/services/identity/src/EShopOnAbp.IdentityService.Application/IdentityServiceApplicationModule.cs index cd2aacb1..7c9e2fd2 100644 --- a/services/identity/src/EShopOnAbp.IdentityService.Application/IdentityServiceApplicationModule.cs +++ b/services/identity/src/EShopOnAbp.IdentityService.Application/IdentityServiceApplicationModule.cs @@ -1,4 +1,5 @@ using EShopOnAbp.IdentityService.Keycloak; +using EShopOnAbp.IdentityService.Keycloak.Service; using Microsoft.Extensions.DependencyInjection; using Volo.Abp.AutoMapper; using Volo.Abp.BackgroundJobs; diff --git a/services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/KeycloakService.cs b/services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/KeycloakService.cs deleted file mode 100644 index 175d5d7d..00000000 --- a/services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/KeycloakService.cs +++ /dev/null @@ -1,85 +0,0 @@ -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Keycloak.Net; -using Keycloak.Net.Models.Roles; -using Keycloak.Net.Models.Users; -using Microsoft.Extensions.Options; -using Volo.Abp.DependencyInjection; - -namespace EShopOnAbp.IdentityService.Keycloak; - -public class KeycloakService : ITransientDependency -{ - private readonly KeycloakClient _keycloakClient; - private readonly KeycloakClientOptions _keycloakOptions; - - public KeycloakService(IOptions keycloakOptions) - { - _keycloakOptions = keycloakOptions.Value; - - _keycloakClient = new KeycloakClient( - _keycloakOptions.Url, - _keycloakOptions.AdminUserName, - _keycloakOptions.AdminPassword - ); - } - - public Task> GetUsersAsync(string search = null, string username = null, string email = null, - CancellationToken cancellationToken = default) - { - return _keycloakClient.GetUsersAsync(_keycloakOptions.RealmName, search: search, username: username, - email: email, cancellationToken: cancellationToken); - } - - public Task GetUserAsync(string userId, CancellationToken cancellationToken = default) - { - return _keycloakClient.GetUserAsync(_keycloakOptions.RealmName, userId, cancellationToken: cancellationToken); - } - - public Task CreateUserAsync(User user, CancellationToken cancellationToken = default) - { - return _keycloakClient.CreateUserAsync(_keycloakOptions.RealmName, user, cancellationToken); - } - - public Task UpdateUserAsync(string userId, User user, CancellationToken cancellationToken = default) - { - return _keycloakClient.UpdateUserAsync(_keycloakOptions.RealmName, userId, user, cancellationToken); - } - - public Task DeleteUserAsync(string userId, CancellationToken cancellationToken = default) - { - return _keycloakClient.DeleteUserAsync(_keycloakOptions.RealmName, userId, cancellationToken); - } - - public Task> GetRolesAsync(CancellationToken cancellationToken = default) - { - return _keycloakClient.GetRolesAsync(_keycloakOptions.RealmName, cancellationToken: cancellationToken); - } - - public Task AddRolesToUserAsync(string userId, IEnumerable roles, - CancellationToken cancellationToken = default) - { - return _keycloakClient.AddRealmRoleMappingsToUserAsync(_keycloakOptions.RealmName, userId, roles, - cancellationToken); - } - - public Task CreateRoleAsync(string name, CancellationToken cancellationToken = default) - { - Role role = new Role - { - Name = name - }; - return _keycloakClient.CreateRoleAsync(_keycloakOptions.RealmName, role, cancellationToken); - } - - public Task DeleteRoleByIdAsync(string id, CancellationToken cancellationToken = default) - { - return _keycloakClient.DeleteRoleByIdAsync(_keycloakOptions.RealmName, id, cancellationToken); - } - - public Task UpdateRoleAsync(string id, Role role, CancellationToken cancellationToken = default) - { - return _keycloakClient.UpdateRoleByIdAsync(_keycloakOptions.RealmName, id, role, cancellationToken); - } -} \ No newline at end of file diff --git a/services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/Service/CachedKeycloakRole.cs b/services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/Service/CachedKeycloakRole.cs new file mode 100644 index 00000000..82e298e6 --- /dev/null +++ b/services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/Service/CachedKeycloakRole.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using Volo.Abp.Caching; + +namespace EShopOnAbp.IdentityService.Keycloak.Service; + +[CacheName("KeycloakRole")] +public class CachedKeycloakRole +{ + public string Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public bool? Composite { get; set; } + public CachedRoleComposite Composites { get; set; } + public bool? ClientRole { get; set; } + public string ContainerId { get; set; } + public IDictionary Attributes { get; set; } +} + +public class CachedRoleComposite +{ + public IDictionary Client { get; set; } + public IEnumerable Realm { get; set; } +} \ No newline at end of file diff --git a/services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/Service/CachedKeycloakService.cs b/services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/Service/CachedKeycloakService.cs new file mode 100644 index 00000000..64e06aec --- /dev/null +++ b/services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/Service/CachedKeycloakService.cs @@ -0,0 +1,127 @@ +// using System.Collections.Generic; +// using System.Linq; +// using System.Threading; +// using System.Threading.Tasks; +// using Keycloak.Net.Models.Roles; +// using Keycloak.Net.Models.Users; +// using Volo.Abp.Caching; +// using Volo.Abp.DependencyInjection; +// +// namespace EShopOnAbp.IdentityService.Keycloak.Service; +// +// [ExposeServices(typeof(CachedKeycloakService))] +// public class CachedKeycloakService : IKeycloakService +// { +// protected const string UsersCacheKey = "KeycloakUsers"; +// protected const string RolesCacheKey = "KeycloakRoles"; +// private readonly IKeycloakService _keycloakService; +// private readonly IDistributedCache> _keycloakUsersCache; +// private readonly IDistributedCache> _keycloakRolesCache; +// +// public CachedKeycloakService(IKeycloakService keycloakService, +// IDistributedCache> keycloakUsersCache, +// IDistributedCache> keycloakRolesCache) +// { +// _keycloakService = keycloakService; +// _keycloakUsersCache = keycloakUsersCache; +// _keycloakRolesCache = keycloakRolesCache; +// } +// +// public async Task> GetUsersAsync(string search = null, string username = null, +// string email = null, +// CancellationToken cancellationToken = default) +// { +// var users = await _keycloakUsersCache.GetAsync(UsersCacheKey, token: cancellationToken); +// if (users == null) +// { +// users = (await _keycloakService.GetUsersAsync(search, username, email, cancellationToken)).ToList(); +// await _keycloakUsersCache.SetAsync(UsersCacheKey, users, token: cancellationToken); +// } +// +// return users; +// } +// +// public async Task CreateUserAsync(User user, CancellationToken cancellationToken = default) +// { +// var result = await _keycloakService.CreateUserAsync(user, cancellationToken); +// if (result) +// { +// await _keycloakUsersCache.RemoveAsync(UsersCacheKey, token: cancellationToken); +// } +// +// return result; +// } +// +// public async Task UpdateUserAsync(string userId, User user, CancellationToken cancellationToken = default) +// { +// var result = await _keycloakService.UpdateUserAsync(userId, user, cancellationToken); +// if (result) +// { +// await _keycloakUsersCache.RemoveAsync(UsersCacheKey, token: cancellationToken); +// } +// +// return result; +// } +// +// public async Task DeleteUserAsync(string userId, CancellationToken cancellationToken = default) +// { +// var result = await _keycloakService.DeleteUserAsync(userId, cancellationToken); +// if (result) +// { +// await _keycloakUsersCache.RemoveAsync(UsersCacheKey, token: cancellationToken); +// } +// +// return result; +// } +// +// public async Task> GetRolesAsync(CancellationToken cancellationToken = default) +// { +// var roles = await _keycloakRolesCache.GetAsync(RolesCacheKey, token: cancellationToken); +// if (roles == null) +// { +// roles = (await _keycloakService.GetRolesAsync(cancellationToken: cancellationToken)).ToList(); +// await _keycloakRolesCache.SetAsync(RolesCacheKey, roles, token: cancellationToken); +// } +// +// return roles; +// } +// +// public Task AddRolesToUserAsync(string userId, IEnumerable roles, +// CancellationToken cancellationToken = default) +// { +// return _keycloakService.AddRolesToUserAsync(userId, roles, cancellationToken); +// } +// +// public async Task CreateRoleAsync(string name, CancellationToken cancellationToken = default) +// { +// var result = await _keycloakService.CreateRoleAsync(name, cancellationToken); +// if (result) +// { +// await _keycloakRolesCache.RemoveAsync(RolesCacheKey, token: cancellationToken); +// } +// +// return result; +// } +// +// public async Task DeleteRoleByIdAsync(string id, CancellationToken cancellationToken = default) +// { +// var result = await _keycloakService.DeleteRoleByIdAsync(id, cancellationToken); +// if (result) +// { +// await _keycloakRolesCache.RemoveAsync(RolesCacheKey, token: cancellationToken); +// } +// +// return result; +// } +// +// public async Task UpdateRoleAsync(string id, Role role, CancellationToken cancellationToken = default) +// { +// var result = await _keycloakService.UpdateRoleAsync(id, role, cancellationToken); +// if (result) +// { +// await _keycloakRolesCache.RemoveAsync(RolesCacheKey, token: cancellationToken); +// } +// +// return result; +// } +// } \ No newline at end of file diff --git a/services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/Service/CachedKeycloakUser.cs b/services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/Service/CachedKeycloakUser.cs new file mode 100644 index 00000000..671cd8f7 --- /dev/null +++ b/services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/Service/CachedKeycloakUser.cs @@ -0,0 +1,75 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using Volo.Abp.Caching; + +namespace EShopOnAbp.IdentityService.Keycloak.Service; + +[CacheName("KeycloakUser")] +public class CachedKeycloakUser +{ + public string Id { get; set; } + public long CreatedTimestamp { get; set; } + public string UserName { get; set; } + public bool? Enabled { get; set; } + public bool? Totp { get; set; } + public bool? EmailVerified { get; set; } + public string FirstName { get; set; } + public string LastName { get; set; } + public string Email { get; set; } + public Collection DisableableCredentialTypes { get; set; } + public Collection RequiredActions { get; set; } + public int? NotBefore { get; set; } + public Dictionary> Attributes { get; set; } + public IDictionary ClientRoles { get; set; } + public string FederationLink { get; set; } + public IEnumerable Groups { get; set; } + public string Origin { get; set; } + public string[] RealmRoles { get; set; } + public string Self { get; set; } + public string ServiceAccountClientId { get; set; } + public CachedUserAccess Access { get; set; } + public IEnumerable ClientConsents { get; set; } + public IEnumerable Credentials { get; set; } + public IEnumerable FederatedIdentities { get; set; } +} + +public class CachedUserConsent +{ + public string ClientId { get; set; } + public IEnumerable GrantedClientScopes { get; set; } + public long? CreatedDate { get; set; } + public long? LastUpdatedDate { get; set; } +} + +public class CachedUserAccess +{ + public bool? ManageGroupMembership { get; set; } + public bool? View { get; set; } + public bool? MapRoles { get; set; } + public bool? Impersonate { get; set; } + public bool? Manage { get; set; } +} + +public class CachedCredentials +{ + public string Algorithm { get; set; } + public IDictionary Config { get; set; } + public int? Counter { get; set; } + public long? CreatedDate { get; set; } + public string Device { get; set; } + public int? Digits { get; set; } + public int? HashIterations { get; set; } + public string HashSaltedValue { get; set; } + public int? Period { get; set; } + public string Salt { get; set; } + public bool? Temporary { get; set; } + public string Type { get; set; } + public string Value { get; set; } +} + +public class CachedFederatedIdentity +{ + public string IdentityProvider { get; set; } + public string UserId { get; set; } + public string UserName { get; set; } +} \ No newline at end of file diff --git a/services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/Service/IKeycloakService.cs b/services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/Service/IKeycloakService.cs new file mode 100644 index 00000000..12bf5b7e --- /dev/null +++ b/services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/Service/IKeycloakService.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Keycloak.Net.Models.Roles; +using Keycloak.Net.Models.Users; +using Volo.Abp.DependencyInjection; + +namespace EShopOnAbp.IdentityService.Keycloak.Service; + +/* + * This will be an external service from the Keycloak package. + * Keeping it under Application layer because the Keycloak.Net.Core package requires .Net6 target framework. + * Application.Contracts targets netstandard2.0 + */ +public interface IKeycloakService : ITransientDependency +{ + Task> GetUsersAsync(string search = null, string username = null, string email = null, CancellationToken cancellationToken = default); + + Task CreateUserAsync(User user, CancellationToken cancellationToken = default); + + Task UpdateUserAsync(string userId, User user, CancellationToken cancellationToken = default); + + Task DeleteUserAsync(string userId, CancellationToken cancellationToken = default); + + Task> GetRolesAsync(CancellationToken cancellationToken = default); + + Task AddRealmRolesToUserAsync(string userId, IEnumerable roles, CancellationToken cancellationToken = default); + public Task> GetRealmRolesOfUserAsync(string userId, CancellationToken cancellationToken = default); + + Task RemoveRealmRolesFromUserAsync(string userId, IEnumerable roles, CancellationToken cancellationToken = default); + + Task CreateRoleAsync(string name, CancellationToken cancellationToken = default); + + Task DeleteRoleByIdAsync(string id, CancellationToken cancellationToken = default); + + Task UpdateRoleAsync(string id, Role role, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/KeycloakClientOptions.cs b/services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/Service/KeycloakClientOptions.cs similarity index 78% rename from services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/KeycloakClientOptions.cs rename to services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/Service/KeycloakClientOptions.cs index 93850fa2..9498cf56 100644 --- a/services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/KeycloakClientOptions.cs +++ b/services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/Service/KeycloakClientOptions.cs @@ -1,4 +1,4 @@ -namespace EShopOnAbp.IdentityService.Keycloak; +namespace EShopOnAbp.IdentityService.Keycloak.Service; public class KeycloakClientOptions { public string Url { get; set; } diff --git a/services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/Service/KeycloakService.cs b/services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/Service/KeycloakService.cs new file mode 100644 index 00000000..23ed6caa --- /dev/null +++ b/services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/Service/KeycloakService.cs @@ -0,0 +1,182 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Keycloak.Net; +using Keycloak.Net.Models.Roles; +using Keycloak.Net.Models.Users; +using Microsoft.Extensions.Options; +using Volo.Abp.Caching; +using Volo.Abp.DependencyInjection; +using Volo.Abp.ObjectMapping; + +namespace EShopOnAbp.IdentityService.Keycloak.Service; + +/* + * This will be an external service from the Keycloak package + */ +[ExposeServices(typeof(IKeycloakService), typeof(KeycloakService))] +public class KeycloakService : IKeycloakService +{ + protected const string UsersCacheKey = "KeycloakUsers"; + protected const string RolesCacheKey = "KeycloakRoles"; + private readonly IObjectMapper _objectMapper; + private readonly IDistributedCache, string> _keycloakUsersCache; + private readonly IDistributedCache, string> _keycloakRolesCache; + private readonly IDistributedCache, string> _userRolesCache; + + private readonly KeycloakClient _keycloakClient; + private readonly KeycloakClientOptions _keycloakOptions; + + public KeycloakService( + IOptions keycloakOptions, + IObjectMapper objectMapper, + IDistributedCache, string> keycloakUsersCache, + IDistributedCache, string> keycloakRolesCache, + IDistributedCache, string> userRolesCache) + { + _objectMapper = objectMapper; + _keycloakUsersCache = keycloakUsersCache; + _keycloakRolesCache = keycloakRolesCache; + _userRolesCache = userRolesCache; + + _keycloakOptions = keycloakOptions.Value; + _keycloakClient = new KeycloakClient( + _keycloakOptions.Url, + _keycloakOptions.AdminUserName, + _keycloakOptions.AdminPassword + ); + } + + public async Task> GetUsersAsync(string search = null, string username = null, + string email = null, + CancellationToken cancellationToken = default) + { + var users = await _keycloakUsersCache.GetAsync(UsersCacheKey, token: cancellationToken); + if (users == null) + { + var result = await _keycloakClient.GetUsersAsync(_keycloakOptions.RealmName, search: search, + username: username, + email: email, cancellationToken: cancellationToken); + users = _objectMapper.Map, List>(result.ToList()); + await _keycloakUsersCache.SetAsync(UsersCacheKey, users, token: cancellationToken); + } + + return users; + } + + public async Task CreateUserAsync(User user, CancellationToken cancellationToken = default) + { + var result = await _keycloakClient.CreateUserAsync(_keycloakOptions.RealmName, user, cancellationToken); + if (result) + { + await _keycloakUsersCache.RemoveAsync(UsersCacheKey, token: cancellationToken); + } + + return result; + } + + public async Task UpdateUserAsync(string userId, User user, CancellationToken cancellationToken = default) + { + var result = await _keycloakClient.UpdateUserAsync(_keycloakOptions.RealmName, userId, user, cancellationToken); + if (result) + { + await _keycloakUsersCache.RemoveAsync(UsersCacheKey, token: cancellationToken); + } + +// _keycloakClient.DeleteRealmRoleMappingsFromUserAsync() + return result; + } + + public async Task DeleteUserAsync(string userId, CancellationToken cancellationToken = default) + { + var result = await _keycloakClient.DeleteUserAsync(_keycloakOptions.RealmName, userId, cancellationToken); + if (result) + { + await _keycloakUsersCache.RemoveAsync(UsersCacheKey, token: cancellationToken); + } + + return result; + } + + public async Task> GetRolesAsync(CancellationToken cancellationToken = default) + { + var roles = await _keycloakRolesCache.GetAsync(RolesCacheKey, token: cancellationToken); + if (roles == null) + { + var result = + (await _keycloakClient.GetRolesAsync(_keycloakOptions.RealmName, cancellationToken: cancellationToken)) + .ToList(); + roles = _objectMapper.Map, List>(result.ToList()); + + await _keycloakRolesCache.SetAsync(RolesCacheKey, roles, token: cancellationToken); + } + + return roles; + } + + public Task AddRealmRolesToUserAsync(string userId, IEnumerable roles, + CancellationToken cancellationToken = default) + { + return _keycloakClient.AddRealmRoleMappingsToUserAsync(_keycloakOptions.RealmName, userId, roles, + cancellationToken); + } + + // Updating user with roles is not working + public async Task> GetRealmRolesOfUserAsync(string userId, + CancellationToken cancellationToken = default) + { + var userRoles = await _userRolesCache.GetAsync(userId, token: cancellationToken); + if (userRoles == null) + { + var roles = (await _keycloakClient.GetRealmRoleMappingsForUserAsync(_keycloakOptions.RealmName, userId, + cancellationToken)) + .ToList(); + userRoles = _objectMapper.Map, List>(roles); + await _userRolesCache.SetAsync(userId, userRoles, token: cancellationToken); + } + + return userRoles; + } + + public Task RemoveRealmRolesFromUserAsync(string userId, IEnumerable roles, + CancellationToken cancellationToken = default) + { + return _keycloakClient.DeleteRealmRoleMappingsFromUserAsync(_keycloakOptions.RealmName, userId, roles, + cancellationToken); + } + + public async Task CreateRoleAsync(string name, CancellationToken cancellationToken = default) + { + var result = await _keycloakClient.CreateRoleAsync(_keycloakOptions.RealmName, new Role() { Name = name }, + cancellationToken); + if (result) + { + await _keycloakRolesCache.RemoveAsync(RolesCacheKey, token: cancellationToken); + } + + return result; + } + + public async Task DeleteRoleByIdAsync(string id, CancellationToken cancellationToken = default) + { + var result = await _keycloakClient.DeleteRoleByIdAsync(_keycloakOptions.RealmName, id, cancellationToken); + if (result) + { + await _keycloakRolesCache.RemoveAsync(RolesCacheKey, token: cancellationToken); + } + + return result; + } + + public async Task UpdateRoleAsync(string id, Role role, CancellationToken cancellationToken = default) + { + var result = await _keycloakClient.UpdateRoleByIdAsync(_keycloakOptions.RealmName, id, role, cancellationToken); + if (result) + { + await _keycloakRolesCache.RemoveAsync(RolesCacheKey, token: cancellationToken); + } + + return result; + } +} \ No newline at end of file diff --git a/services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/Service/KeycloakServiceExtensions.cs b/services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/Service/KeycloakServiceExtensions.cs new file mode 100644 index 00000000..01474c1b --- /dev/null +++ b/services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/Service/KeycloakServiceExtensions.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; +using Keycloak.Net.Models.Roles; +using Keycloak.Net.Models.Users; + +namespace EShopOnAbp.IdentityService.Keycloak.Service; + +public static class KeycloakServiceExtensions +{ + public static string GenerateCacheKeyBasedOnValues(this IEnumerable roles) + { + return GenerateUniqueCacheKeyBasedOnList(roles); + } + + public static string GenerateCacheKeyBasedOnValues(this IEnumerable users) + { + return GenerateUniqueCacheKeyBasedOnList(users); + } + + private static string GenerateUniqueCacheKeyBasedOnList(IEnumerable list) + { + string serializedList = JsonSerializer.Serialize(list); + byte[] bytes = Encoding.UTF8.GetBytes(serializedList); + byte[] hash = SHA256.Create().ComputeHash(bytes); + string hashString = BitConverter.ToString(hash).Replace("-", ""); + return hashString; + } +} \ No newline at end of file From deadcc0ee16666ffeccfd714434d4c78b416998b Mon Sep 17 00:00:00 2001 From: Galip Tolga Erdem Date: Wed, 26 Jul 2023 21:06:38 -0400 Subject: [PATCH 8/9] Fixed user update issues --- .../Users/KeycloakUserUpdatingJob.cs | 40 +++++++++++++++---- .../Identity/EShopIdentityUserAppService.cs | 5 +++ .../Keycloak/Service/IKeycloakService.cs | 3 +- .../Keycloak/Service/KeycloakService.cs | 25 +----------- .../Service/KeycloakServiceExtensions.cs | 1 + 5 files changed, 42 insertions(+), 32 deletions(-) diff --git a/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/Users/KeycloakUserUpdatingJob.cs b/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/Users/KeycloakUserUpdatingJob.cs index 5308ed5e..f3bc74a4 100644 --- a/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/Users/KeycloakUserUpdatingJob.cs +++ b/services/identity/src/EShopOnAbp.IdentityService.Application/BackgroundJobs/Users/KeycloakUserUpdatingJob.cs @@ -3,6 +3,7 @@ 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; @@ -18,7 +19,8 @@ public class KeycloakUserUpdatingJob : AsyncBackgroundJob _logger; private readonly IObjectMapper _objectMapper; - public KeycloakUserUpdatingJob(IKeycloakService keycloakService, ILogger logger, IObjectMapper objectMapper) + public KeycloakUserUpdatingJob(IKeycloakService keycloakService, ILogger logger, + IObjectMapper objectMapper) { _keycloakService = keycloakService; _logger = logger; @@ -30,11 +32,11 @@ public class KeycloakUserUpdatingJob : AsyncBackgroundJob q.UserName == args.UserName); + .FirstOrDefault(q => q.UserName == args.OldUserName); if (keycloakUser == null) { - _logger.LogError($"Keycloak user could not be found to update! Username:{args.UserName}"); - throw new UserFriendlyException($"Keycloak user with the username:{args.UserName} could not be found!"); + _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 differentFields = args.GetDifferentFields().ToList(); @@ -42,7 +44,7 @@ public class KeycloakUserUpdatingJob : AsyncBackgroundJob(keycloakUser); + var result = await _keycloakService.UpdateUserAsync( - keycloakUser.Id, + 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>(oldRoles)); + } + + if (newRoles.Count > 0) + { + await _keycloakService.AddRealmRolesToUserAsync(keycloakUser.Id, + _objectMapper.Map, List>(newRoles)); + } + } + if (result) { _logger.LogInformation($"Keycloak user with the username:{args.UserName} has been updated."); diff --git a/services/identity/src/EShopOnAbp.IdentityService.Application/Identity/EShopIdentityUserAppService.cs b/services/identity/src/EShopOnAbp.IdentityService.Application/Identity/EShopIdentityUserAppService.cs index 2f10bfe7..f56f7d4a 100644 --- a/services/identity/src/EShopOnAbp.IdentityService.Application/Identity/EShopIdentityUserAppService.cs +++ b/services/identity/src/EShopOnAbp.IdentityService.Application/Identity/EShopIdentityUserAppService.cs @@ -43,6 +43,11 @@ public class EShopIdentityUserAppService : IdentityUserAppService public override async Task 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); diff --git a/services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/Service/IKeycloakService.cs b/services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/Service/IKeycloakService.cs index 12bf5b7e..aa09d8d3 100644 --- a/services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/Service/IKeycloakService.cs +++ b/services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/Service/IKeycloakService.cs @@ -25,9 +25,8 @@ public interface IKeycloakService : ITransientDependency Task> GetRolesAsync(CancellationToken cancellationToken = default); Task AddRealmRolesToUserAsync(string userId, IEnumerable roles, CancellationToken cancellationToken = default); - public Task> GetRealmRolesOfUserAsync(string userId, CancellationToken cancellationToken = default); - Task RemoveRealmRolesFromUserAsync(string userId, IEnumerable roles, CancellationToken cancellationToken = default); + Task RemoveRealmRolesFromUserAsync(string userId, IEnumerable roles, CancellationToken cancellationToken = default); Task CreateRoleAsync(string name, CancellationToken cancellationToken = default); diff --git a/services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/Service/KeycloakService.cs b/services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/Service/KeycloakService.cs index 23ed6caa..73f2b41b 100644 --- a/services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/Service/KeycloakService.cs +++ b/services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/Service/KeycloakService.cs @@ -23,7 +23,6 @@ public class KeycloakService : IKeycloakService private readonly IObjectMapper _objectMapper; private readonly IDistributedCache, string> _keycloakUsersCache; private readonly IDistributedCache, string> _keycloakRolesCache; - private readonly IDistributedCache, string> _userRolesCache; private readonly KeycloakClient _keycloakClient; private readonly KeycloakClientOptions _keycloakOptions; @@ -32,13 +31,11 @@ public class KeycloakService : IKeycloakService IOptions keycloakOptions, IObjectMapper objectMapper, IDistributedCache, string> keycloakUsersCache, - IDistributedCache, string> keycloakRolesCache, - IDistributedCache, string> userRolesCache) + IDistributedCache, string> keycloakRolesCache) { _objectMapper = objectMapper; _keycloakUsersCache = keycloakUsersCache; _keycloakRolesCache = keycloakRolesCache; - _userRolesCache = userRolesCache; _keycloakOptions = keycloakOptions.Value; _keycloakClient = new KeycloakClient( @@ -84,7 +81,6 @@ public class KeycloakService : IKeycloakService await _keycloakUsersCache.RemoveAsync(UsersCacheKey, token: cancellationToken); } -// _keycloakClient.DeleteRealmRoleMappingsFromUserAsync() return result; } @@ -122,24 +118,7 @@ public class KeycloakService : IKeycloakService cancellationToken); } - // Updating user with roles is not working - public async Task> GetRealmRolesOfUserAsync(string userId, - CancellationToken cancellationToken = default) - { - var userRoles = await _userRolesCache.GetAsync(userId, token: cancellationToken); - if (userRoles == null) - { - var roles = (await _keycloakClient.GetRealmRoleMappingsForUserAsync(_keycloakOptions.RealmName, userId, - cancellationToken)) - .ToList(); - userRoles = _objectMapper.Map, List>(roles); - await _userRolesCache.SetAsync(userId, userRoles, token: cancellationToken); - } - - return userRoles; - } - - public Task RemoveRealmRolesFromUserAsync(string userId, IEnumerable roles, + public Task RemoveRealmRolesFromUserAsync(string userId, IEnumerable roles, CancellationToken cancellationToken = default) { return _keycloakClient.DeleteRealmRoleMappingsFromUserAsync(_keycloakOptions.RealmName, userId, roles, diff --git a/services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/Service/KeycloakServiceExtensions.cs b/services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/Service/KeycloakServiceExtensions.cs index 01474c1b..21840fbe 100644 --- a/services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/Service/KeycloakServiceExtensions.cs +++ b/services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/Service/KeycloakServiceExtensions.cs @@ -8,6 +8,7 @@ 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 roles) From 3e4d29744a1a84fc2ecab66f666acf4d3ba447b4 Mon Sep 17 00:00:00 2001 From: Galip Tolga Erdem Date: Wed, 26 Jul 2023 21:08:05 -0400 Subject: [PATCH 9/9] removed unused service --- .../Keycloak/Service/CachedKeycloakService.cs | 127 ------------------ 1 file changed, 127 deletions(-) delete mode 100644 services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/Service/CachedKeycloakService.cs diff --git a/services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/Service/CachedKeycloakService.cs b/services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/Service/CachedKeycloakService.cs deleted file mode 100644 index 64e06aec..00000000 --- a/services/identity/src/EShopOnAbp.IdentityService.Application/Keycloak/Service/CachedKeycloakService.cs +++ /dev/null @@ -1,127 +0,0 @@ -// using System.Collections.Generic; -// using System.Linq; -// using System.Threading; -// using System.Threading.Tasks; -// using Keycloak.Net.Models.Roles; -// using Keycloak.Net.Models.Users; -// using Volo.Abp.Caching; -// using Volo.Abp.DependencyInjection; -// -// namespace EShopOnAbp.IdentityService.Keycloak.Service; -// -// [ExposeServices(typeof(CachedKeycloakService))] -// public class CachedKeycloakService : IKeycloakService -// { -// protected const string UsersCacheKey = "KeycloakUsers"; -// protected const string RolesCacheKey = "KeycloakRoles"; -// private readonly IKeycloakService _keycloakService; -// private readonly IDistributedCache> _keycloakUsersCache; -// private readonly IDistributedCache> _keycloakRolesCache; -// -// public CachedKeycloakService(IKeycloakService keycloakService, -// IDistributedCache> keycloakUsersCache, -// IDistributedCache> keycloakRolesCache) -// { -// _keycloakService = keycloakService; -// _keycloakUsersCache = keycloakUsersCache; -// _keycloakRolesCache = keycloakRolesCache; -// } -// -// public async Task> GetUsersAsync(string search = null, string username = null, -// string email = null, -// CancellationToken cancellationToken = default) -// { -// var users = await _keycloakUsersCache.GetAsync(UsersCacheKey, token: cancellationToken); -// if (users == null) -// { -// users = (await _keycloakService.GetUsersAsync(search, username, email, cancellationToken)).ToList(); -// await _keycloakUsersCache.SetAsync(UsersCacheKey, users, token: cancellationToken); -// } -// -// return users; -// } -// -// public async Task CreateUserAsync(User user, CancellationToken cancellationToken = default) -// { -// var result = await _keycloakService.CreateUserAsync(user, cancellationToken); -// if (result) -// { -// await _keycloakUsersCache.RemoveAsync(UsersCacheKey, token: cancellationToken); -// } -// -// return result; -// } -// -// public async Task UpdateUserAsync(string userId, User user, CancellationToken cancellationToken = default) -// { -// var result = await _keycloakService.UpdateUserAsync(userId, user, cancellationToken); -// if (result) -// { -// await _keycloakUsersCache.RemoveAsync(UsersCacheKey, token: cancellationToken); -// } -// -// return result; -// } -// -// public async Task DeleteUserAsync(string userId, CancellationToken cancellationToken = default) -// { -// var result = await _keycloakService.DeleteUserAsync(userId, cancellationToken); -// if (result) -// { -// await _keycloakUsersCache.RemoveAsync(UsersCacheKey, token: cancellationToken); -// } -// -// return result; -// } -// -// public async Task> GetRolesAsync(CancellationToken cancellationToken = default) -// { -// var roles = await _keycloakRolesCache.GetAsync(RolesCacheKey, token: cancellationToken); -// if (roles == null) -// { -// roles = (await _keycloakService.GetRolesAsync(cancellationToken: cancellationToken)).ToList(); -// await _keycloakRolesCache.SetAsync(RolesCacheKey, roles, token: cancellationToken); -// } -// -// return roles; -// } -// -// public Task AddRolesToUserAsync(string userId, IEnumerable roles, -// CancellationToken cancellationToken = default) -// { -// return _keycloakService.AddRolesToUserAsync(userId, roles, cancellationToken); -// } -// -// public async Task CreateRoleAsync(string name, CancellationToken cancellationToken = default) -// { -// var result = await _keycloakService.CreateRoleAsync(name, cancellationToken); -// if (result) -// { -// await _keycloakRolesCache.RemoveAsync(RolesCacheKey, token: cancellationToken); -// } -// -// return result; -// } -// -// public async Task DeleteRoleByIdAsync(string id, CancellationToken cancellationToken = default) -// { -// var result = await _keycloakService.DeleteRoleByIdAsync(id, cancellationToken); -// if (result) -// { -// await _keycloakRolesCache.RemoveAsync(RolesCacheKey, token: cancellationToken); -// } -// -// return result; -// } -// -// public async Task UpdateRoleAsync(string id, Role role, CancellationToken cancellationToken = default) -// { -// var result = await _keycloakService.UpdateRoleAsync(id, role, cancellationToken); -// if (result) -// { -// await _keycloakRolesCache.RemoveAsync(RolesCacheKey, token: cancellationToken); -// } -// -// return result; -// } -// } \ No newline at end of file