Skip to content

Commit

Permalink
feat: Support workday manager structure (#721)
Browse files Browse the repository at this point in the history
- [x] New feature
- [ ] Bug fix
- [ ] High impact

**Description of work:**
Managers structure is changed when workday is rolled out. The manager will no longer be in the department he/she is maanger for. This means they are moved one level up.

Logics we have where `isResourceOwner` is combined with `fullDepartment` cannot be used as is.


**Testing:**
- [ ] Can be tested
- [ ] Automatic tests created / updated
- [ ] Local tests are passing

TBD, tests must be refactored as setup for manager structure will be
more complex


**Checklist:**
- [ ] Considered automated tests
- [ ] Considered updating specification / documentation
- [ ] Considered work items 
- [ ] Considered security
- [ ] Performed developer testing
- [ ] Checklist finalized / ready for review

---------

Co-authored-by: Jonathan Idland Olsnes <73334350+Jonathanio123@users.noreply.github.com>
  • Loading branch information
HansDahle and Jonathanio123 authored Nov 20, 2024
1 parent dd57b94 commit 687eac1
Show file tree
Hide file tree
Showing 38 changed files with 1,170 additions and 543 deletions.
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'
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
{
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
@@ -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
{
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)
.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
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
@@ -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?.FullDepartment != null)
{
claims.Add(new Claim(ResourcesClaimTypes.ResourceOwnerForDepartment, orgUnit.FullDepartment));
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)
{
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

0 comments on commit 687eac1

Please sign in to comment.