Skip to content

Commit

Permalink
Keycloak Refactor (#3624)
Browse files Browse the repository at this point in the history
* Refactored keycloak sync to use the keycloak repository used by the API. ALso removed tools.Core and removed redundant models

* pr fixes
  • Loading branch information
FuriousLlama authored Dec 1, 2023
1 parent 9be0933 commit 26c199e
Show file tree
Hide file tree
Showing 41 changed files with 560 additions and 899 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public static class ServiceCollectionExtensions
public static IServiceCollection AddPimsKeycloakService(this IServiceCollection services)
{
return services.AddScoped<IPimsKeycloakService, PimsKeycloakService>()
.AddScoped<IKeycloakService, KeycloakService>();
.AddScoped<IKeycloakRepository, KeycloakRepository>();
}
}
}
16 changes: 8 additions & 8 deletions source/backend/dal.keycloak/PimsKeycloakService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ namespace Pims.Dal.Keycloak
public class PimsKeycloakService : IPimsKeycloakService
{
#region Variable
private readonly IKeycloakService _keycloakService;
private readonly IKeycloakRepository _keycloakRepository;
private readonly IUserRepository _userRepository;
private readonly IRoleRepository _roleRepository;
private readonly IAccessRequestRepository _accessRequestRepository;
Expand All @@ -33,19 +33,19 @@ public class PimsKeycloakService : IPimsKeycloakService
/// <summary>
/// Creates a new instance of a PimsKeycloakService object, initializes with the specified arguments.
/// </summary>
/// <param name="keycloakService"></param>
/// <param name="keycloakRepository"></param>
/// <param name="userRepository"></param>
/// <param name="roleRepository"></param>
/// <param name="accessRequestRepository"></param>
/// <param name="user"></param>
public PimsKeycloakService(
IKeycloakService keycloakService,
IKeycloakRepository keycloakRepository,
IUserRepository userRepository,
IRoleRepository roleRepository,
IAccessRequestRepository accessRequestRepository,
ClaimsPrincipal user)
{
_keycloakService = keycloakService;
_keycloakRepository = keycloakRepository;
_userRepository = userRepository;
_roleRepository = roleRepository;
_accessRequestRepository = accessRequestRepository;
Expand All @@ -63,7 +63,7 @@ public PimsKeycloakService(
/// <returns></returns>
public async Task<Entity.PimsUser> UpdateUserAsync(Entity.PimsUser user)
{
var kuser = await _keycloakService.GetUserAsync(user.GuidIdentifierValue.Value) ?? throw new KeyNotFoundException("User does not exist in Keycloak");
var kuser = await _keycloakRepository.GetUserAsync(user.GuidIdentifierValue.Value) ?? throw new KeyNotFoundException("User does not exist in Keycloak");
var euser = _userRepository.GetTrackingById(user.Internal_Id);

return await SaveUserChanges(user, euser, kuser, true);
Expand All @@ -77,7 +77,7 @@ public PimsKeycloakService(
/// <returns></returns>
public async Task<Entity.PimsUser> AppendToUserAsync(Entity.PimsUser update)
{
var kuser = await _keycloakService.GetUserAsync(update.GuidIdentifierValue.Value) ?? throw new KeyNotFoundException("User does not exist in Keycloak");
var kuser = await _keycloakRepository.GetUserAsync(update.GuidIdentifierValue.Value) ?? throw new KeyNotFoundException("User does not exist in Keycloak");
var euser = _userRepository.GetTrackingById(update.Internal_Id);

return await SaveUserChanges(update, euser, kuser, true);
Expand Down Expand Up @@ -165,13 +165,13 @@ public PimsKeycloakService(
var roles = update.IsDisabled.HasValue && update.IsDisabled.Value ? System.Array.Empty<PimsRole>() : euser.PimsUserRoles.Select(ur => _roleRepository.Find(ur.RoleId));

// Now update keycloak
var keycloakUserGroups = await _keycloakService.GetUserGroupsAsync(euser.GuidIdentifierValue.Value);
var keycloakUserGroups = await _keycloakRepository.GetUserGroupsAsync(euser.GuidIdentifierValue.Value);
var newRolesToAdd = roles.Where(r => keycloakUserGroups.All(crr => crr.Name != r.Name));
var rolesToRemove = keycloakUserGroups.Where(r => roles.All(crr => crr.Name != r.Name));
var addOperations = newRolesToAdd.Select(nr => new UserRoleOperation() { Operation = "add", RoleName = nr.Name, Username = update.GetIdirUsername() });
var removeOperations = rolesToRemove.Select(rr => new UserRoleOperation() { Operation = "del", RoleName = rr.Name, Username = update.GetIdirUsername() });

await _keycloakService.ModifyUserRoleMappings(addOperations.Concat(removeOperations));
await _keycloakRepository.ModifyUserRoleMappings(addOperations.Concat(removeOperations));
_userRepository.CommitTransaction();

return _userRepository.GetById(euser.Internal_Id);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ public static class ServiceCollectionExtensions
/// </summary>
/// <param name="services"></param>
/// <returns></returns>
public static IServiceCollection AddKeycloakService(this IServiceCollection services)
public static IServiceCollection AddKeycloakRepository(this IServiceCollection services)
{
return services.AddScoped<IKeycloakService, KeycloakService>();
return services.AddScoped<IKeycloakRepository, KeycloakRepository>();
}
}
}
40 changes: 40 additions & 0 deletions source/backend/keycloak/IKeycloakRepository.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
using Pims.Keycloak.Models;

namespace Pims.Keycloak
{
public interface IKeycloakRepository
{
#region Users

Task<UserModel> GetUserAsync(Guid id);

Task<List<UserModel>> GetUsersAsync(Guid id);

Task<HttpResponseMessage> AddRolesToUser(string username, IEnumerable<RoleModel> roles);

Task<HttpResponseMessage> DeleteRoleFromUsers(string username, string roleName);

Task<RoleModel[]> GetUserGroupsAsync(Guid id);

Task ModifyUserRoleMappings(IEnumerable<UserRoleOperation> operations);

Task<ResponseWrapper<RoleModel>> GetAllRoles();

Task<ResponseWrapper<RoleModel>> GetAllGroupRoles(string groupName);

Task<ResponseWrapper<RoleModel>> GetUserRoles(string username);

Task<HttpResponseMessage> AddKeycloakRole(RoleModel role);

Task<HttpResponseMessage> AddKeycloakRolesToGroup(string groupName, IEnumerable<RoleModel> roles);

Task<HttpResponseMessage> DeleteRole(string roleName);

Task<HttpResponseMessage> DeleteRoleFromGroup(string groupName, string roleName);
#endregion
}
}
19 changes: 0 additions & 19 deletions source/backend/keycloak/IKeycloakService.cs

This file was deleted.

179 changes: 179 additions & 0 deletions source/backend/keycloak/KeycloakRepository.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.Options;
using Pims.Core.Extensions;
using Pims.Core.Http;
using Pims.Keycloak.Extensions;
using Pims.Keycloak.Models;

namespace Pims.Keycloak
{
/// <summary>
/// KeycloakRepository class, provides a service for sending HTTP requests to the keycloak admin API.
/// - https://www.keycloak.org/docs-api/5.0/rest-api/index.html#_overview.
/// </summary>
public partial class KeycloakRepository : IKeycloakRepository
{
#region Variables
private readonly IOpenIdConnectRequestClient _client;
#endregion

#region Properties

/// <summary>
/// get - The configuration options for keycloak.
/// </summary>
public Configuration.KeycloakOptions Options { get; }
#endregion

#region Constructors

/// <summary>
/// Creates a new instance of a KeycloakAdmin class, initializes it with the specified arguments.
/// </summary>
/// <param name="client"></param>
/// <param name="options"></param>
public KeycloakRepository(IOpenIdConnectRequestClient client, IOptions<Configuration.KeycloakOptions> options)
{
this.Options = options.Value;
this.Options.Validate();
this.Options.ServiceAccount.Validate();
_client = client;
_client.AuthClientOptions.Audience = this.Options.ServiceAccount.Audience ?? this.Options.Audience;
_client.AuthClientOptions.Authority = this.Options.ServiceAccount.Authority ?? this.Options.Authority;
_client.AuthClientOptions.Client = this.Options.ServiceAccount.Client;
_client.AuthClientOptions.Secret = this.Options.ServiceAccount.Secret;
}

/// <summary>
/// Get the user for the specified 'id'.
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
public async Task<UserModel> GetUserAsync(Guid id)
{
var users = await GetUsersAsync(id);
return users.FirstOrDefault();
}

public async Task<List<UserModel>> GetUsersAsync(Guid id)
{
var response = await _client.GetAsync($"{this.Options.ServiceAccount.Api}/{this.Options.ServiceAccount.Environment}/idir/users?guid={id.ToString().Replace("-", string.Empty)}");
var result = await response.HandleResponseAsync<ResponseWrapper<UserModel>>();

return result.Data.ToList();
}

public async Task<HttpResponseMessage> AddRolesToUser(string username, IEnumerable<RoleModel> roles)
{
return await _client.PostJsonAsync($"{GetIntegrationUrl()}/users/{Uri.EscapeDataString(username)}/roles", roles);
}

public async Task<HttpResponseMessage> DeleteRoleFromUsers(string username, string roleName)
{
return await _client.DeleteAsync($"{GetIntegrationUrl()}/users/{Uri.EscapeDataString(username)}/roles/{Uri.EscapeDataString(roleName)}");
}

/// <summary>
/// Get an array of the groups the user for the specified 'id' is a member of.
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
public async Task<RoleModel[]> GetUserGroupsAsync(Guid id)
{
var response = await _client.GetAsync($"{GetIntegrationUrl()}/user-role-mappings/?username={id.ToString().Replace("-", string.Empty)}@idir");

var userRoleModel = await response.HandleResponseAsync<UserRoleModel>();

return userRoleModel.Roles.Where(r => r.Composite.HasValue && r.Composite.Value).ToArray();
}

/// <summary>
/// Get the total number of groups the user for the specified 'id' is a member of.
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
public async Task<int> GetUserGroupCountAsync(Guid id)
{
var response = await GetUserGroupsAsync(id);
return response.Length;
}

/// <summary>
/// execute all passed operations.
/// </summary>
/// <param name="operations"></param>
/// <returns></returns>
public async Task ModifyUserRoleMappings(IEnumerable<UserRoleOperation> operations)
{
foreach (UserRoleOperation operation in operations)
{
var json = operation.Serialize();
using var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await _client.PostAsync($"{GetIntegrationUrl()}/user-role-mappings", content);
await response.HandleResponseAsync<UserRoleModel>();
}
}

public async Task<ResponseWrapper<RoleModel>> GetAllRoles()
{
var response = await _client.GetAsync($"{GetIntegrationUrl()}/roles");

var allKeycloakRoles = await response.HandleResponseAsync<ResponseWrapper<RoleModel>>();
return allKeycloakRoles;
}

public async Task<ResponseWrapper<RoleModel>> GetAllGroupRoles(string groupName)
{
var response = await _client.GetAsync($"{GetIntegrationUrl()}/roles/{Uri.EscapeDataString(groupName)}/composite-roles");

var groupedRoles = await response.HandleResponseAsync<ResponseWrapper<RoleModel>>();
return groupedRoles;
}

public async Task<ResponseWrapper<RoleModel>> GetUserRoles(string username)
{
var response = await _client.GetAsync($"{GetIntegrationUrl()}/users/{Uri.EscapeDataString(username)}/roles");

var groupedRoles = await response.HandleResponseAsync<ResponseWrapper<RoleModel>>();
return groupedRoles;
}

public async Task<HttpResponseMessage> AddKeycloakRole(RoleModel role)
{
var response = await _client.PostJsonAsync($"{GetIntegrationUrl()}/roles", role);
return response;
}

public async Task<HttpResponseMessage> AddKeycloakRolesToGroup(string groupName, IEnumerable<RoleModel> roles)
{
var response = await _client.PostJsonAsync($"{GetIntegrationUrl()}/roles/{Uri.EscapeDataString(groupName)}/composite-roles", roles);
return response;
}

public async Task<HttpResponseMessage> DeleteRole(string roleName)
{
var response = await _client.DeleteAsync($"{GetIntegrationUrl()}/roles/{Uri.EscapeDataString(roleName)}");
return response;
}

public async Task<HttpResponseMessage> DeleteRoleFromGroup(string groupName, string roleName)
{
var response = await _client.DeleteAsync($"{GetIntegrationUrl()}/roles/{Uri.EscapeDataString(groupName)}/composite-roles/{Uri.EscapeDataString(roleName)}");
return response;
}

private string GetIntegrationUrl()
{
return $"{this.Options.ServiceAccount.Api}/integrations/{this.Options.ServiceAccount.Integration}/{this.Options.ServiceAccount.Environment}";
}
#endregion

#region Methods
#endregion
}
}
47 changes: 0 additions & 47 deletions source/backend/keycloak/KeycloakService.cs

This file was deleted.

2 changes: 1 addition & 1 deletion source/backend/keycloak/Models/RoleModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public class RoleModel
/// <summary>
/// get/set - whether or not this role is a composite role.
/// </summary>
public bool Composite { get; set; }
public bool? Composite { get; set; }
#endregion
}
}
Loading

0 comments on commit 26c199e

Please sign in to comment.