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

fix: Fixed profile and relevant-department to resolve correctly after workday update #725

Merged
merged 3 commits into from
Nov 24, 2024
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
@@ -1,9 +1,44 @@
namespace Fusion.Resources.Domain.Models
using System.Collections.Generic;
using System;

namespace Fusion.Resources.Domain.Models
{
internal class QueryOrgUnitReason
{
public QueryOrgUnitReason(string fullDepartment, string reason)
{
IsWildCard = fullDepartment.Trim().EndsWith('*');
Reason = reason;
FullDepartment = fullDepartment.Replace("*", "").Trim();
Level = FullDepartment.Split(" ").Length;
IsGlobalRole = string.IsNullOrEmpty(fullDepartment);

}

public string FullDepartment { get; set; } = null!;
public string Reason { get; set; } = null!;
public bool IsWildCard => FullDepartment.Contains('*') ;
public bool IsWildCard { get; set; }
public int Level { get; set; }
public bool IsGlobalRole { get; set; }
}
}

internal struct OrgUnitComparer
{
public OrgUnitComparer(string FullDepartment)
{
this.FullDepartment = FullDepartment;
Level = FullDepartment.Split(" ").Length;
}

public string FullDepartment { get; }
public int Level { get; set; }

public bool IsChildOf(OrgUnitComparer other, int maxDistance)
{
var isChild = other.FullDepartment.StartsWith(FullDepartment + " ", StringComparison.OrdinalIgnoreCase);
var distance = Level - other.Level;

return isChild && distance <= maxDistance;
}
}
}
242 changes: 137 additions & 105 deletions src/backend/api/Fusion.Resources.Domain/Queries/GetRelevantOrgUnits.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
using Fusion.AspNetCore.OData;
using Azure.Core;
using Fusion.AspNetCore.OData;
using Fusion.Integration;
using Fusion.Integration.Profile;
using Fusion.Resources.Domain.Models;
using MediatR;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
Expand All @@ -29,11 +31,13 @@ public GetRelevantOrgUnits(string profileId, AspNetCore.OData.ODataQueryParams q

public class Handler : IRequestHandler<GetRelevantOrgUnits, QueryRangedList<QueryRelevantOrgUnit>?>
{
private readonly IMediator mediator;
private readonly IFusionProfileResolver profileResolver;
private readonly IOrgUnitCache orgUnitCache;

public Handler(IFusionProfileResolver profileResolver, IOrgUnitCache orgUnitCache)
public Handler(IMediator mediator, IFusionProfileResolver profileResolver, IOrgUnitCache orgUnitCache)
{
this.mediator = mediator;
this.profileResolver = profileResolver;
this.orgUnitCache = orgUnitCache;
}
Expand All @@ -48,29 +52,32 @@ public Handler(IFusionProfileResolver profileResolver, IOrgUnitCache orgUnitCach
FullDepartment = x.FullDepartment,
Department = x.Department,
ShortName = x.ShortName
});
}).ToList();

var user = await profileResolver.ResolvePersonFullProfileAsync(request.ProfileId.OriginalIdentifier);

if (user?.Roles is null)
throw new InvalidOperationException("Roles was not loaded for profile. Required to resolve profile manager responsebility.");

if (user?.FullDepartment is null)
{
return null;
}

var orgUnitAccessReason = new List<QueryOrgUnitReason>();

orgUnitAccessReason.ApplyManager(user);
var managerForUnits = await ResolveUserManagerUnitAsync(user);

orgUnitAccessReason.AddRange(managerForUnits);


// Filter out only active roles
var activeRoles = user.Roles.Where(x => x.IsActive);
var activeRoles = user.Roles.Where(x => x.IsActive && (x.OnDemandSupport == false || x.ActiveToUtc > DateTime.UtcNow));

var delegatedManagerClaims = activeRoles.Where(x => x.Name.StartsWith("Fusion.Resources.ResourceOwner")).Select(x => x.Scope?.Value);
var delegatedManagerClaims = activeRoles.Where(x => x.Name.StartsWith("Fusion.Resources.ResourceOwner")).Where(x => x.Scope?.Value is not null).Select(x => x.Scope?.Value!);
orgUnitAccessReason.ApplyRole(delegatedManagerClaims, ReasonRoles.DelegatedManager);

var adminClaims = activeRoles?.Where(x => x.Name.StartsWith("Fusion.Resources.Full") || x.Name.StartsWith("Fusion.Resources.Admin")).Select(x => x.Scope?.Value);
// Must support global roles
var adminClaims = activeRoles.Where(x => x.Name.StartsWith("Fusion.Resources.Full") || x.Name.StartsWith("Fusion.Resources.Admin")).Select(x => x.Scope?.Value ?? "*");
orgUnitAccessReason.ApplyRole(adminClaims, ReasonRoles.Write);

var readClaims = activeRoles?.Where(x => x.Name.StartsWith("Fusion.Resources.Request") || x.Name.StartsWith("Fusion.Resources.Read")).Select(x => x.Scope?.Value);
var readClaims = activeRoles.Where(x => x.Name.StartsWith("Fusion.Resources.Request") || x.Name.StartsWith("Fusion.Resources.Read")).Select(x => x.Scope?.Value ?? "*");
orgUnitAccessReason.ApplyRole(readClaims, ReasonRoles.Read);

orgUnitAccessReason.ApplyParentManager(orgUnits, user);
Expand All @@ -82,7 +89,8 @@ public Handler(IFusionProfileResolver profileResolver, IOrgUnitCache orgUnitCach

List<QueryRelevantOrgUnit> populatedOrgUnitResult = GetRelevantOrgUnits(orgUnits, orgUnitAccessReason);

var filteredOrgUnits = ApplyOdataFilters(request.Query, populatedOrgUnitResult.OrderBy(x => x.SapId));
var filteredOrgUnits = ApplyOdataFilters(request.Query, populatedOrgUnitResult.OrderBy(x => x.FullDepartment));

var skip = request.Query.Skip.GetValueOrDefault(0);
var take = request.Query.Top.GetValueOrDefault(100);

Expand All @@ -91,132 +99,156 @@ public Handler(IFusionProfileResolver profileResolver, IOrgUnitCache orgUnitCach
return pagedQuery;
}

private static List<QueryRelevantOrgUnit> GetRelevantOrgUnits(IEnumerable<QueryRelevantOrgUnit> cachedOrgUnits, List<QueryOrgUnitReason> orgUnitAccessReason)
private async Task<IEnumerable<QueryOrgUnitReason>> ResolveUserManagerUnitAsync(FusionFullPersonProfile user)
{
var endResult = new List<QueryRelevantOrgUnit>();
foreach (var org in orgUnitAccessReason)
{
if (user.Roles is null)
throw new InvalidOperationException("Roles was not loaded for profile. Required to resolve profile manager responsebility.");

if (org?.FullDepartment != null && org?.Reason != null)
{
var alreadyInList = endResult.FirstOrDefault(x => x.FullDepartment == org.FullDepartment);
var managerRoles = user.Roles
.Where(x => string.Equals(x.Name, "Fusion.LineOrg.Manager", StringComparison.OrdinalIgnoreCase))
.Where(x => !string.IsNullOrEmpty(x.Scope?.Value))
.Select(x => x.Scope?.Value!)
.ToList();

if (alreadyInList is null)
{
QueryRelevantOrgUnit? data = new();
if (org.IsWildCard == true)
{
data = cachedOrgUnits.FirstOrDefault(x => x.FullDepartment == org.FullDepartment.Replace('*', ' ').TrimEnd());
}
else
{
data = cachedOrgUnits.FirstOrDefault(x => x.FullDepartment == org.FullDepartment || x.Department == org.FullDepartment);
}
if (data != null)
{
data.Reasons.Add(org.Reason);
endResult.Add(data);
}
}
else
{
if (!alreadyInList.Reasons.Contains(org.Reason))
{
alreadyInList.Reasons.Add(org.Reason);
}
}
var managerFor = new List<QueryOrgUnitReason>();

foreach (var orgUnitId in managerRoles)
{
var orgUnit = await mediator.Send(new ResolveLineOrgUnit(orgUnitId));
if (orgUnit?.FullDepartment != null)
{
managerFor.Add(new QueryOrgUnitReason(orgUnit.FullDepartment, ReasonRoles.Manager));
}
}
return endResult;

return managerFor;
}

private static List<QueryRelevantOrgUnit> ApplyOdataFilters(ODataQueryParams filter, IEnumerable<QueryRelevantOrgUnit> orgUnits)
private static List<QueryRelevantOrgUnit> GetRelevantOrgUnits(IEnumerable<QueryRelevantOrgUnit> cachedOrgUnits, List<QueryOrgUnitReason> orgUnitAccessReason)
{

var filterGenerator = filter.GenerateFilters<QueryRelevantOrgUnit>(m =>
{
m.SqlQueryMode = false;
m.MapField("sapId", e => e.SapId);
m.MapField("name", e => e.Name);
m.MapField("shortName", e => e.ShortName);
m.MapField("department", e => e.Department);
m.MapField("fullDepartment", e => e.FullDepartment);
m.MapField("reason", e => e.Reasons);
});
return orgUnits.Where(filterGenerator.FilterLambda.Compile()).ToList();
orgUnitAccessReason.GroupBy(i => i.FullDepartment)
.ToList()
.ForEach(d =>
{
var orgUnit = cachedOrgUnits.FirstOrDefault(o => string.Equals(o.FullDepartment, d.Key, StringComparison.OrdinalIgnoreCase));
if (orgUnit is not null)
{
orgUnit.Reasons = d.Select(i => i.Reason).ToList();
}
});

return cachedOrgUnits.Where(i => i.Reasons.Any()).ToList();
}

private static List<QueryRelevantOrgUnit> ApplyOdataFilters(ODataQueryParams filter, IEnumerable<QueryRelevantOrgUnit> orgUnits)
{
var query = orgUnits.AsQueryable()
.ApplyODataFilters(filter, m =>
{
m.SqlQueryMode = false;
m.MapField("sapId", e => e.SapId);
m.MapField("name", e => e.Name);
m.MapField("shortName", e => e.ShortName);
m.MapField("department", e => e.Department);
m.MapField("fullDepartment", e => e.FullDepartment);

// Disabling reasons, as this is not supported by the filter.
//m.MapField("reason", e => e.Reasons);
})
.ApplyODataSorting(filter, m =>
{
m.MapField("sapId", e => e.SapId);
m.MapField("name", e => e.Name);
m.MapField("shortName", e => e.ShortName);
m.MapField("department", e => e.Department);
m.MapField("fullDepartment", e => e.FullDepartment);
},
q => q.OrderBy(o => o.FullDepartment));

return query.ToList();
}
}
}

internal static class OrgUnitAccessReasons
{
internal static void ApplyManager(this List<QueryOrgUnitReason> reasons, FusionFullPersonProfile user)

public static bool IsDirectChildOf(this QueryRelevantOrgUnit orgUnit, QueryOrgUnitReason unit)
{
var isDepartmentManager = user.IsResourceOwner;
if (isDepartmentManager)
reasons.Add(new QueryOrgUnitReason
{
FullDepartment = user?.FullDepartment ?? "",
Reason = ReasonRoles.Manager
});
var item = new OrgUnitComparer(orgUnit.FullDepartment);
var distance = item.Level - unit.Level;

return item.FullDepartment.StartsWith(unit.FullDepartment) && distance == 1;
}

internal static void ApplyRole(this List<QueryOrgUnitReason> reasons, IEnumerable<string?>? departments, string role)
internal static void ApplyRole(this List<QueryOrgUnitReason> reasons, IEnumerable<string> departments, string role)
{
if (departments is not null)
{
reasons.AddRange(departments.Select(dep => new QueryOrgUnitReason
{
FullDepartment = dep ?? "*",
Reason = role
}));
}
reasons.AddRange(departments.Select(d => new QueryOrgUnitReason(d, role)));
}

internal static void ApplyParentManager(this List<QueryOrgUnitReason> reasons, IEnumerable<QueryRelevantOrgUnit> orgUnits, FusionFullPersonProfile user)
{
var managerResposibility = new List<QueryOrgUnitReason>();
var managerOrDelegatedManagerDepartmentsOrWildcard = reasons
.Where(x => x.Reason.Equals(ReasonRoles.Manager) || x.Reason.Equals(ReasonRoles.DelegatedManager) || x.IsWildCard).ToList();
if (managerOrDelegatedManagerDepartmentsOrWildcard is not null)

// Process locations where user is natural manager. We want to grant the user access to all child departments.
foreach (var managerUnit in reasons.Where(x => x.Reason == ReasonRoles.Manager))
{
foreach (var department in managerOrDelegatedManagerDepartmentsOrWildcard)
{
var parentDepartment = department.FullDepartment.Replace("*", "").TrimEnd();
var childDepartments = orgUnits
.Where(x => x.FullDepartment.StartsWith(managerUnit.FullDepartment))
.Where(x => x.IsDirectChildOf(managerUnit)); // Include trailing space so we do not include the actual unit where user is manager.

var childDepartments = orgUnits.Distinct().Where(x => x.FullDepartment.StartsWith(parentDepartment) && !x.FullDepartment.Equals(parentDepartment));
managerResposibility.AddRange(childDepartments.Select(d => new QueryOrgUnitReason(d.FullDepartment, ReasonRoles.ParentManager)));
}

// if the department is not of type wildcard we only want to get direct children (one level below)
if (!department.IsWildCard)
{
var getParentDepartmentLevel = GetAcronymsForDepartment(parentDepartment);
// Process delegate manager.
foreach (var delegatedManager in reasons.Where(x => x.Reason == ReasonRoles.DelegatedManager))
{
var childDepartments = orgUnits.Where(x => x.FullDepartment.StartsWith(delegatedManager.FullDepartment + " "));

childDepartments = childDepartments.Where(x => (GetAcronymsForDepartment(x.FullDepartment).Count() == getParentDepartmentLevel.Length + 1));
if (delegatedManager.IsWildCard)
{
// Add all child org units as delegated manager role.
managerResposibility.AddRange(childDepartments.Select(d => new QueryOrgUnitReason(d.FullDepartment, ReasonRoles.DelegatedManager)));
}
else
{
// Add just direct children
managerResposibility.AddRange(childDepartments.Where(d => d.IsDirectChildOf(delegatedManager)).Select(d => new QueryOrgUnitReason(d.FullDepartment, ReasonRoles.DelegatedParentManager)));
}
}

}
var reason = ReasonRoles.DelegatedParentManager;
if (user.IsResourceOwner && user.FullDepartment == parentDepartment)
{
reason = ReasonRoles.ParentManager;
}
// Process read/write roles

foreach (var child in childDepartments)
{
managerResposibility?.Add(new QueryOrgUnitReason
{
FullDepartment = child.FullDepartment,
Reason = reason
});
}
foreach (var delegatedManager in reasons.Where(x => x.Reason == ReasonRoles.Write && x.IsWildCard))
{
if (delegatedManager.IsGlobalRole)
{
managerResposibility.AddRange(orgUnits.Select(d => new QueryOrgUnitReason(d.FullDepartment, ReasonRoles.Write)));
}
reasons?.AddRange(managerResposibility);
else
{
var childDepartments = orgUnits.Where(x => x.FullDepartment.StartsWith(delegatedManager.FullDepartment) && x.FullDepartment != delegatedManager.FullDepartment);
managerResposibility.AddRange(childDepartments.Select(d => new QueryOrgUnitReason(d.FullDepartment, ReasonRoles.Write)));
}

}
}

static string[] GetAcronymsForDepartment(string word)
{
return word.Split();
foreach (var delegatedManager in reasons.Where(x => x.Reason == ReasonRoles.Read && x.IsWildCard))
{
if (delegatedManager.IsGlobalRole)
{
managerResposibility.AddRange(orgUnits.Select(d => new QueryOrgUnitReason(d.FullDepartment, ReasonRoles.Read)));
}
else
{
var childDepartments = orgUnits.Where(x => x.FullDepartment.StartsWith(delegatedManager.FullDepartment) && x.FullDepartment != delegatedManager.FullDepartment);
managerResposibility.AddRange(childDepartments.Select(d => new QueryOrgUnitReason(d.FullDepartment, ReasonRoles.Read)));
}
}

// Mutate at the end
reasons.AddRange(managerResposibility);
}
}
}
Loading
Loading