Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Keycloak Refactor #3624

Merged
merged 3 commits into from
Dec 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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; }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was this incorrect in the current implementation?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was inconsistent between the two models

#endregion
}
}
Loading
Loading