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

feat: Support workday manager structure #721

Merged
merged 28 commits into from
Nov 20, 2024
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
8be5cd4
Updated claimstransformer to add claims based on roles instead of pro…
HansDahle Sep 23, 2024
9c067db
Updated claimstransformer to add claims based on roles instead of pro…
HansDahle Sep 23, 2024
a3e4099
refactored
HansDahle Sep 23, 2024
fd8c069
Changed auth to new requirement
HansDahle Sep 23, 2024
8b7d166
WIP manager test setup
HansDahle Nov 11, 2024
c70b1cc
Merge branch 'feat/workday-manager-structure-support' of https://gith…
HansDahle Nov 11, 2024
7e52f32
Fixed security matrix tests
HansDahle Nov 12, 2024
20684ad
Updated tests to construct manager correctly
HansDahle Nov 14, 2024
15e923a
Fixed tests, refactored how manager for departments is fetched
HansDahle Nov 14, 2024
b0713da
Fixed tests by fixing mocking setup
HansDahle Nov 14, 2024
690ccaf
Fixed tests
HansDahle Nov 14, 2024
6c57fd0
Fixed & refactored tests
HansDahle Nov 14, 2024
d7e3057
Changed test department to avoid collision
HansDahle Nov 14, 2024
575d890
Added support for emulate access
HansDahle Nov 15, 2024
ad6f4c0
Added options with emulate to personnel
HansDahle Nov 15, 2024
24e42bf
Fixed duplicate options endpoint
HansDahle Nov 15, 2024
66197c5
Fixed endpoint not working due to combination and required params
HansDahle Nov 15, 2024
5600feb
Merge remote-tracking branch 'origin/master' into feat/workday-manage…
HansDahle Nov 15, 2024
2f586d9
Fixed incorrect options endpoint
HansDahle Nov 15, 2024
5f39732
Added tll to pr deploy
HansDahle Nov 15, 2024
ac1053d
Fixed incorrect auth check
HansDahle Nov 15, 2024
2b7247b
Updated old auth usage
HansDahle Nov 15, 2024
30297f1
Fixed response when not allowed to emulate user
HansDahle Nov 15, 2024
ea1cacd
Update src/backend/api/Fusion.Resources.Api/Authentication/ResourcesL…
HansDahle Nov 18, 2024
1e7ef69
Aligned class/filename
HansDahle Nov 18, 2024
001c014
Merge branch 'feat/workday-manager-structure-support' of https://gith…
HansDahle Nov 18, 2024
e099980
Merge remote-tracking branch 'origin/master' into feat/workday-manage…
HansDahle Nov 18, 2024
ab64bc8
Merge remote-tracking branch 'origin/master' into feat/workday-manage…
HansDahle Nov 20, 2024
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
5 changes: 5 additions & 0 deletions src/Fusion.Summary.Api/Deployment/k8s/pr-deployment-env.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ metadata:
labels:
environment: pr
prNumber: '{{prNumber}}'
annotations:
k8s-ttl-controller.twin.sh/ttl: '3d'
HansDahle marked this conversation as resolved.
Show resolved Hide resolved
spec:
replicas: 1
strategy:
Expand Down Expand Up @@ -96,6 +98,8 @@ metadata:
labels:
environment: pr
prNumber: '{{prNumber}}'
annotations:
k8s-ttl-controller.twin.sh/ttl: '3d'

spec:
selector:
Expand All @@ -113,6 +117,7 @@ metadata:
environment: pr
prNumber: '{{prNumber}}'
annotations:
k8s-ttl-controller.twin.sh/ttl: '3d'
nginx.ingress.kubernetes.io/rewrite-target: /
nginx.ingress.kubernetes.io/proxy-buffer-size: "32k"
nginx.org/client-max-body-size: "50m"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using Fusion.AspNetCore.FluentAuthorization;
using Fusion.Resources.Api.Authorization.Requirements;
using Fusion.Resources.Authorization.Requirements;

namespace Fusion.Resources
{
public static class IAuthorizationRequirementExtensions
HansDahle marked this conversation as resolved.
Show resolved Hide resolved
{
public static IAuthorizationRequirementRule GlobalRoleAccess(this IAuthorizationRequirementRule builder, params string[] roles)
{
return builder.AddRule(new GlobalRoleRequirement(roles));
}
public static IAuthorizationRequirementRule AllGlobalRoleAccess(this IAuthorizationRequirementRule builder, params string[] roles)
{
return builder.AddRule(new GlobalRoleRequirement(GlobalRoleRequirement.RoleRequirement.All, roles));
}

/// <summary>
/// Require that the user is a resource owner.
/// The check uses the resource owner claims in the user profile.
/// </summary>
/// <remarks>
/// <para>
/// To include additional local adjustments a local claims transformer can be used to add new claims.
/// Type="http://schemas.fusion.equinor.com/identity/claims/resourceowner" value="MY DEP PATH"
/// </para>
/// <para>
/// The parents check will only work for the direct path. Other resource owners in sibling departments of a parent will not have access.
/// Ex. Check "L1 L2.1 L3.1 L4.1", owner in L2.1 L3.1, L2.1, L1 will have access, but ex. L2.2 will not have.
/// </para>
/// </remarks>
/// <param name="builder"></param>
/// <param name="includeParents">Should resource owners in any of the direct parent departments have access</param>
/// <param name="includeDescendants">Should anyone that is a resource owner in any of the sub departments have access</param>
public static IAuthorizationRequirementRule BeResourceOwnerForDepartment(this IAuthorizationRequirementRule builder, string department, bool includeParents = false, bool includeDescendants = false)
{
builder.AddRule(new BeResourceOwnerRequirement(department, includeParents, includeDescendants));
return builder;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
using Fusion.Authorization;
using Microsoft.AspNetCore.Authorization;

namespace Fusion.Resources.Api.Authorization
namespace Fusion.Resources.Api.Authorization.Requirements
{
public class GlobalRoleRequirement : FusionAuthorizationRequirement, IAuthorizationHandler
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
using Fusion.Authorization;
using Microsoft.AspNetCore.Authorization;
using System;
using System.Linq;
using System.Threading.Tasks;

namespace Fusion.Resources.Authorization.Requirements
{
/// <summary>
/// Adjustment of the same type provided by the intergration lib.
/// This will work against a claim added by the local transformer. This will resolve the role provided by the line org for manager responsebility based on SAP data.
/// This update is harder to update in the integration lib claims transformer, due to optimalization.
/// </summary>
public class BeResourceOwnerRequirement : FusionAuthorizationRequirement, IAuthorizationHandler
Jonathanio123 marked this conversation as resolved.
Show resolved Hide resolved
{
public BeResourceOwnerRequirement(string departmentPath, bool includeParents = false, bool includeDescendants = false)
{
DepartmentPath = departmentPath;
IncludeParents = includeParents;
IncludeDescendants = includeDescendants;
}

public BeResourceOwnerRequirement()
{
}


public override string Description => ToString();

public override string Code => "ResourceOwner";

public string? DepartmentPath { get; }
public bool IncludeParents { get; }
public bool IncludeDescendants { get; }

public Task HandleAsync(AuthorizationHandlerContext context)
{
var departments = context.User.FindAll(ResourcesClaimTypes.ResourceOwnerForDepartment)
HansDahle marked this conversation as resolved.
Show resolved Hide resolved
.Select(c => c.Value);

if (!departments.Any())
{
SetEvaluation("User is not resource owner in any departments");
return Task.CompletedTask;
}
if (string.IsNullOrEmpty(DepartmentPath))
{
context.Succeed(this);
return Task.CompletedTask;
}

// responsibility descendant Descendants
var directResponsibility = departments.Any(d => d.Equals(DepartmentPath, StringComparison.OrdinalIgnoreCase));
var descendantResponsibility = departments.Any(d => d.StartsWith(DepartmentPath, StringComparison.OrdinalIgnoreCase));
var parentResponsibility = departments.Any(d => DepartmentPath.StartsWith(d, StringComparison.OrdinalIgnoreCase));

var hasAccess = directResponsibility
|| IncludeParents && parentResponsibility
|| IncludeDescendants && descendantResponsibility;

if (hasAccess)
{
SetEvaluation($"User has access though responsibility in {string.Join(", ", departments)}. " +
$"[owner in department={directResponsibility}, parents={parentResponsibility}, descendants={descendantResponsibility}]");

context.Succeed(this);
}

SetEvaluation($"User have responsibility in departments: {string.Join(", ", departments)}; But not in the requirement '{DepartmentPath}'");

return Task.CompletedTask;
}

public override string ToString()
{
if (string.IsNullOrEmpty(DepartmentPath))
return "User must be resource owner of a department";

if (IncludeParents && IncludeDescendants)
return $"User must be resource owner in department '{DepartmentPath}' or any departments above or below";

if (IncludeParents)
return $"User must be resource owner in department '{DepartmentPath}' or any departments above";

if (IncludeDescendants)
return $"User must be resource owner in department '{DepartmentPath}' or any sub departments";

return $"User must be resource owner in department '{DepartmentPath}'";
}
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
using Fusion.Integration.Authentication;
using Fusion.Integration.Profile;
using Fusion.Resources.Api.Authorization;
using Fusion.Resources.Database;
using Fusion.Resources.Domain;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
Expand All @@ -14,11 +16,15 @@ namespace Fusion.Resources.Api.Authentication
public class ResourcesLocalClaimsTransformation : ILocalClaimsTransformation
{
private static Task<IEnumerable<Claim>> noClaims = Task.FromResult<IEnumerable<Claim>>(Array.Empty<Claim>());
private readonly ILogger<ResourcesLocalClaimsTransformation> logger;
private readonly ResourcesDbContext db;
private readonly IMediator mediator;

public ResourcesLocalClaimsTransformation(ResourcesDbContext db)
public ResourcesLocalClaimsTransformation(ILogger<ResourcesLocalClaimsTransformation> logger, ResourcesDbContext db, IMediator mediator)
{
this.logger = logger;
this.db = db;
this.mediator = mediator;
}

public Task<IEnumerable<Claim>> TransformApplicationAsync(ClaimsPrincipal principal, FusionApplicationProfile profile)
Expand All @@ -36,15 +42,38 @@ public async Task<IEnumerable<Claim>> TransformUserAsync(ClaimsPrincipal princip
return claims;
}

private static Task ApplyResourceOwnerForDepartmentClaimIfUserIsResourceOwnerAsync(FusionFullPersonProfile profile, List<Claim> claims)
private async Task ApplyResourceOwnerForDepartmentClaimIfUserIsResourceOwnerAsync(FusionFullPersonProfile profile, List<Claim> claims)
{
if (profile.IsResourceOwner && !string.IsNullOrEmpty(profile.FullDepartment))
{
claims.Add(new Claim(ResourcesClaimTypes.ResourceOwnerForDepartment, profile.FullDepartment));
// This will now point to incorrect department. We need to use the roles on the profile, to see scoped manager responsebility.
// Leaving in for reference.
//if (profile.IsResourceOwner && !string.IsNullOrEmpty(profile.FullDepartment))
//{
// claims.Add(new Claim(ResourcesClaimTypes.ResourceOwnerForDepartment, profile.FullDepartment));
//}

if (profile.Roles is null) {
throw new InvalidOperationException("Roles must be loaded on the profile for the claims transformer to work.");
}

return Task.CompletedTask;
}
var managerRoles = profile.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();

// Got a list of sap id's, need to resolve them to the full department to keep consistent.
logger.LogInformation($"Found user responsible for [{managerRoles.Count}] org units [{string.Join(",", managerRoles)}]");

foreach (var orgUnitId in managerRoles)
{
var orgUnit = await mediator.Send(new ResolveLineOrgUnit(orgUnitId));
if (orgUnit != null)
{
claims.Add(new Claim(ResourcesClaimTypes.ResourceOwnerForDepartment, orgUnit.FullDepartment));
HansDahle marked this conversation as resolved.
Show resolved Hide resolved
logger.LogInformation($"Adding claim for {orgUnitId} -> [{orgUnit.FullDepartment}]");
}
}
}

private async Task ApplySharedRequestClaimsIfAnyAsync(FusionFullPersonProfile profile, List<Claim> claims)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
using Fusion.AspNetCore.FluentAuthorization;
using Fusion.Authorization;
using Fusion.Integration;
using Fusion.Integration.Profile;
using Fusion.Resources.Api.Authorization;
using Fusion.Resources.Api.Authorization.Requirements;
using Fusion.Resources.Authorization.Requirements;
using Fusion.Resources.Domain;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Infrastructure;
Expand Down Expand Up @@ -36,14 +38,6 @@ public static IAuthorizationRequirementRule FullControlExternal(this IAuthorizat
return builder;
}

public static IAuthorizationRequirementRule GlobalRoleAccess(this IAuthorizationRequirementRule builder, params string[] roles)
{
return builder.AddRule(new GlobalRoleRequirement(roles));
}
public static IAuthorizationRequirementRule AllGlobalRoleAccess(this IAuthorizationRequirementRule builder, params string[] roles)
{
return builder.AddRule(new GlobalRoleRequirement(GlobalRoleRequirement.RoleRequirement.All, roles));
}
public static IAuthorizationRequirementRule OrgChartPositionWriteAccess(this IAuthorizationRequirementRule builder, Guid orgProjectId, Guid orgPositionId)
HansDahle marked this conversation as resolved.
Show resolved Hide resolved
{
return builder.AddRule(OrgPositionAccessRequirement.OrgPositionWrite(orgProjectId, orgPositionId));
Expand Down Expand Up @@ -71,21 +65,17 @@ public static IAuthorizationRequirementRule RequireConversationForResourceOwner(
}

/// <summary>
/// Indicates that the user is in any way or form a resource owner
/// Requires the user to be resource owner for any department
/// </summary>
/// <param name="builder"></param>
/// <returns></returns>
public static IAuthorizationRequirementRule BeResourceOwner(this IAuthorizationRequirementRule builder)
public static IAuthorizationRequirementRule BeResourceOwnerForAnyDepartment(this IAuthorizationRequirementRule builder)
{
var policy = new AuthorizationPolicyBuilder()
.RequireAssertion(c => c.User.HasClaim(c => c.Type == FusionClaimsTypes.ResourceOwner))
.Build();

builder.AddRule((auth, user) => auth.AuthorizeAsync(user, policy));

builder.AddRule(new BeResourceOwnerRequirement());
return builder;
}


public static IAuthorizationRequirementRule HaveRole(this IAuthorizationRequirementRule builder, string role)
{
var policy = new AuthorizationPolicyBuilder()
Expand All @@ -101,7 +91,7 @@ public static IAuthorizationRequirementRule BeSiblingResourceOwner(this IAuthori
{
// User has access if the parent department matches..
var resourceParent = path.ParentDeparment;
var userDepartments = c.User.GetResponsibleForDepartments();
var userDepartments = c.User.GetManagerForDepartments();

return userDepartments.Any(d => resourceParent.IsDepartment(new DepartmentPath(d).Parent()));
})
Expand All @@ -121,7 +111,7 @@ public static IAuthorizationRequirementRule BeDirectChildResourceOwner(this IAut
var policy = new AuthorizationPolicyBuilder()
.RequireAssertion(c =>
{
var userDepartments = c.User.GetResponsibleForDepartments()
var userDepartments = c.User.GetManagerForDepartments()
.Select(d => new DepartmentPath(d).Parent());

return userDepartments.Any(d => path.IsDepartment(d));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ public async Task<ActionResult<ApiRelatedDepartments>> GetRelevantDepartments([F
}

[HttpOptions("/departments/{departmentString}/delegated-resource-owners")]
[EmulatedUserSupport]
public async Task<ActionResult> GetDelegatedResourceOwnersOptions([FromRoute] OrgUnitIdentifier departmentString)
{
if (!departmentString.Exists)
Expand Down
Loading
Loading