Skip to content

Commit

Permalink
Merge branch 'dev' into features/psp-7304
Browse files Browse the repository at this point in the history
  • Loading branch information
FuriousLlama authored Dec 1, 2023
2 parents bdb5fee + b2db13e commit b423803
Show file tree
Hide file tree
Showing 44 changed files with 565 additions and 905 deletions.
4 changes: 2 additions & 2 deletions source/backend/api/Pims.Api.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<UserSecretsId>0ef6255f-9ea0-49ec-8c65-c172304b4926</UserSecretsId>
<Version>4.0.0-67.25</Version>
<Version>4.0.0-67.25</Version>
<Version>4.0.0-67.27</Version>
<Version>4.0.0-67.27</Version>
<AssemblyVersion>4.0.0.67</AssemblyVersion>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<ProjectGuid>16BC0468-78F6-4C91-87DA-7403C919E646</ProjectGuid>
Expand Down
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 b423803

Please sign in to comment.