From 687eac115f222ac9acffbdae296896655782cd7d Mon Sep 17 00:00:00 2001 From: Hans Dahle Date: Wed, 20 Nov 2024 16:25:18 +0100 Subject: [PATCH] feat: Support workday manager structure (#721) - [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> --- .../Deployment/k8s/pr-deployment-env.yml | 5 + .../IAuthorizationRequirementExtensions.cs | 41 + .../BeResourceOwnerRequirement.cs | 91 +++ .../Requirements/GlobalRoleRequirement.cs | 2 +- .../ResourcesLocalClaimsTransformation.cs | 45 +- .../RequirementsBuilderExtensions.cs | 26 +- .../Departments/DepartmentsController.cs | 1 + .../Controllers/Filter/EmulatedUserSupport.cs | 100 +++ .../Person/PersonAbsenceController.cs | 26 +- .../Controllers/Person/PersonController.cs | 15 +- .../InternalPersonnelController.Preview.cs | 2 +- .../Personnel/InternalPersonnelController.cs | 58 +- .../Requests/ConversationsController.cs | 20 +- .../Requests/DepartmentRequestsController.cs | 80 +- .../Requests/InternalRequestsController.cs | 78 +- .../Requests/RequestActionsController.cs | 20 +- .../Requests/SecondOpinionController.cs | 17 +- .../Requests/ShareRequestsController.cs | 8 +- .../ResponsibilityMatrixController.cs | 13 +- .../Deployment/k8s/pr-deployment-env.yml | 5 + .../Extensions/ClaimsPrincipalExtensions.cs | 16 + .../Fusion.Resources.Api.csproj | 28 +- .../CanApproveStepHandlerBase.cs | 14 +- .../AuthorizationTests/SecurityMatrixTests.cs | 706 ++++++++++-------- .../Data/InternalRequestData.cs | 9 +- .../Fixture/ResourceApiFixture.cs | 58 +- .../DepartmentRequestsTests.cs | 39 +- .../DepartmentsControllerTests.cs | 9 +- .../ProjectRequestAccessTests.cs | 32 + .../IntegrationTests/PersonAbsenceTests.cs | 11 +- .../IntegrationTests/PersonNotesTests.cs | 22 +- .../IntegrationTests/RequestActionTests.cs | 4 +- .../IntegrationTests/RequestComments.cs | 3 +- .../RequestConversationTests.cs | 4 +- .../ResponsibilityMatrixTests.cs | 4 +- .../LineOrgServiceMock.cs | 31 + ...Fusion.Testing.Mocks.ProfileService.csproj | 9 +- .../PeopleServiceMock.cs | 61 ++ 38 files changed, 1170 insertions(+), 543 deletions(-) create mode 100644 src/backend/Fusion.Resources.Authorization/IAuthorizationRequirementExtensions.cs create mode 100644 src/backend/Fusion.Resources.Authorization/Requirements/BeResourceOwnerRequirement.cs rename src/backend/{api/Fusion.Resources.Api/Authorization => Fusion.Resources.Authorization}/Requirements/GlobalRoleRequirement.cs (96%) create mode 100644 src/backend/api/Fusion.Resources.Api/Controllers/Filter/EmulatedUserSupport.cs create mode 100644 src/backend/api/Fusion.Resources.Api/Extensions/ClaimsPrincipalExtensions.cs diff --git a/src/Fusion.Summary.Api/Deployment/k8s/pr-deployment-env.yml b/src/Fusion.Summary.Api/Deployment/k8s/pr-deployment-env.yml index 00258d58b5..203785ee64 100644 --- a/src/Fusion.Summary.Api/Deployment/k8s/pr-deployment-env.yml +++ b/src/Fusion.Summary.Api/Deployment/k8s/pr-deployment-env.yml @@ -24,6 +24,8 @@ metadata: labels: environment: pr prNumber: '{{prNumber}}' + annotations: + k8s-ttl-controller.twin.sh/ttl: '3d' spec: replicas: 1 strategy: @@ -96,6 +98,8 @@ metadata: labels: environment: pr prNumber: '{{prNumber}}' + annotations: + k8s-ttl-controller.twin.sh/ttl: '3d' spec: selector: @@ -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" diff --git a/src/backend/Fusion.Resources.Authorization/IAuthorizationRequirementExtensions.cs b/src/backend/Fusion.Resources.Authorization/IAuthorizationRequirementExtensions.cs new file mode 100644 index 0000000000..f9379f2ea3 --- /dev/null +++ b/src/backend/Fusion.Resources.Authorization/IAuthorizationRequirementExtensions.cs @@ -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)); + } + + /// + /// Require that the user is a resource owner. + /// The check uses the resource owner claims in the user profile. + /// + /// + /// + /// 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" + /// + /// + /// 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. + /// + /// + /// + /// Should resource owners in any of the direct parent departments have access + /// Should anyone that is a resource owner in any of the sub departments have access + public static IAuthorizationRequirementRule BeResourceOwnerForDepartment(this IAuthorizationRequirementRule builder, string department, bool includeParents = false, bool includeDescendants = false) + { + builder.AddRule(new BeResourceOwnerRequirement(department, includeParents, includeDescendants)); + return builder; + } + } +} diff --git a/src/backend/Fusion.Resources.Authorization/Requirements/BeResourceOwnerRequirement.cs b/src/backend/Fusion.Resources.Authorization/Requirements/BeResourceOwnerRequirement.cs new file mode 100644 index 0000000000..c2f143314c --- /dev/null +++ b/src/backend/Fusion.Resources.Authorization/Requirements/BeResourceOwnerRequirement.cs @@ -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 +{ + /// + /// 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. + /// + 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}'"; + } + } +} diff --git a/src/backend/api/Fusion.Resources.Api/Authorization/Requirements/GlobalRoleRequirement.cs b/src/backend/Fusion.Resources.Authorization/Requirements/GlobalRoleRequirement.cs similarity index 96% rename from src/backend/api/Fusion.Resources.Api/Authorization/Requirements/GlobalRoleRequirement.cs rename to src/backend/Fusion.Resources.Authorization/Requirements/GlobalRoleRequirement.cs index e959d44f6c..1d89d64abd 100644 --- a/src/backend/api/Fusion.Resources.Api/Authorization/Requirements/GlobalRoleRequirement.cs +++ b/src/backend/Fusion.Resources.Authorization/Requirements/GlobalRoleRequirement.cs @@ -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 { diff --git a/src/backend/api/Fusion.Resources.Api/Authentication/ResourcesLocalClaimsTransformation.cs b/src/backend/api/Fusion.Resources.Api/Authentication/ResourcesLocalClaimsTransformation.cs index f917b5a53d..a9e9a5e391 100644 --- a/src/backend/api/Fusion.Resources.Api/Authentication/ResourcesLocalClaimsTransformation.cs +++ b/src/backend/api/Fusion.Resources.Api/Authentication/ResourcesLocalClaimsTransformation.cs @@ -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; @@ -14,11 +16,15 @@ namespace Fusion.Resources.Api.Authentication public class ResourcesLocalClaimsTransformation : ILocalClaimsTransformation { private static Task> noClaims = Task.FromResult>(Array.Empty()); + private readonly ILogger logger; private readonly ResourcesDbContext db; + private readonly IMediator mediator; - public ResourcesLocalClaimsTransformation(ResourcesDbContext db) + public ResourcesLocalClaimsTransformation(ILogger logger, ResourcesDbContext db, IMediator mediator) { + this.logger = logger; this.db = db; + this.mediator = mediator; } public Task> TransformApplicationAsync(ClaimsPrincipal principal, FusionApplicationProfile profile) @@ -36,15 +42,38 @@ public async Task> TransformUserAsync(ClaimsPrincipal princip return claims; } - private static Task ApplyResourceOwnerForDepartmentClaimIfUserIsResourceOwnerAsync(FusionFullPersonProfile profile, List claims) + private async Task ApplyResourceOwnerForDepartmentClaimIfUserIsResourceOwnerAsync(FusionFullPersonProfile profile, List 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 claims) { diff --git a/src/backend/api/Fusion.Resources.Api/Authorization/Extensions/RequirementsBuilderExtensions.cs b/src/backend/api/Fusion.Resources.Api/Authorization/Extensions/RequirementsBuilderExtensions.cs index 32ace243d9..841aaf071b 100644 --- a/src/backend/api/Fusion.Resources.Api/Authorization/Extensions/RequirementsBuilderExtensions.cs +++ b/src/backend/api/Fusion.Resources.Api/Authorization/Extensions/RequirementsBuilderExtensions.cs @@ -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; @@ -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)); @@ -71,21 +65,17 @@ public static IAuthorizationRequirementRule RequireConversationForResourceOwner( } /// - /// Indicates that the user is in any way or form a resource owner + /// Requires the user to be resource owner for any department /// /// /// - 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() @@ -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())); }) @@ -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)); diff --git a/src/backend/api/Fusion.Resources.Api/Controllers/Departments/DepartmentsController.cs b/src/backend/api/Fusion.Resources.Api/Controllers/Departments/DepartmentsController.cs index e761cc7d2d..0450332667 100644 --- a/src/backend/api/Fusion.Resources.Api/Controllers/Departments/DepartmentsController.cs +++ b/src/backend/api/Fusion.Resources.Api/Controllers/Departments/DepartmentsController.cs @@ -63,6 +63,7 @@ public async Task> GetRelevantDepartments([F } [HttpOptions("/departments/{departmentString}/delegated-resource-owners")] + [EmulatedUserSupport] public async Task GetDelegatedResourceOwnersOptions([FromRoute] OrgUnitIdentifier departmentString) { if (!departmentString.Exists) diff --git a/src/backend/api/Fusion.Resources.Api/Controllers/Filter/EmulatedUserSupport.cs b/src/backend/api/Fusion.Resources.Api/Controllers/Filter/EmulatedUserSupport.cs new file mode 100644 index 0000000000..9c30db030b --- /dev/null +++ b/src/backend/api/Fusion.Resources.Api/Controllers/Filter/EmulatedUserSupport.cs @@ -0,0 +1,100 @@ +using Fusion.Integration.Http.Errors; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc.Filters; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Fusion.Integration.Http.Models; +using Azure.Core; +using Fusion.AspNetCore.FluentAuthorization; +using Fusion.Integration; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Http; +using System.Security.Claims; + + +namespace Fusion.Resources.Api.Controllers +{ + /// + /// Enables support for overriding the HttpContext.User object with a claims principal constructed using a provided user identifier. + /// + /// The user id is looked for in "?emulatedUserId" query param. Can be wither UPN or azure id. + /// + /// Requires admin access to enter user emulation mode. + /// + /// SHOULD ONLY BE ADDED TO ENDPOINTS THAT DOES NOT PROVIDE DATA ACCESS, EITHER READ OR WRITE! + /// + public class EmulatedUserSupport : ActionFilterAttribute + { + + public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + { + var emulatedUserId = context.HttpContext.Request.Query["emulatedUserId"]; + + if (!string.IsNullOrEmpty(emulatedUserId)) + { + var authorizationService = context.HttpContext.RequestServices.GetRequiredService(); + var authResult = await context.HttpContext.Request.RequireAuthorizationAsync(r => + { + r.AlwaysAccessWhen().GlobalRoleAccess("Fusion.Resources.FullControl"); + r.AlwaysAccessWhen().GlobalRoleAccess("Fusion.Resources.EmulateUser"); + }); + + if (authResult.Unauthorized) + { + context.Result = authResult.CreateForbiddenResponse(); + } + + + var user = await context.HttpContext.GetEmulatedClaimsUserAsync(emulatedUserId!); + + context.HttpContext.User = user; + } + + + await base.OnActionExecutionAsync(context, next); + } + + } + + public static class RequestExtensions + { + + /// + /// Should generate a mostly accurate claims principal based on the user id. + /// Will be missing role claims directly connected to the user through azure ad app registration roles. + /// + /// HttpContext to fetch required services + /// upn or azure unique id. Used to resolve initial profile + /// ClaimsPrincipal with standard fusion claims + public static async Task GetEmulatedClaimsUserAsync(this HttpContext httpContext, string userId) + { + var resolver = httpContext.RequestServices.GetRequiredService(); + var claimsTransformer = httpContext.RequestServices.GetRequiredService(); + + var userProfile = await resolver.ResolvePersonBasicProfileAsync(userId); + + var identity = new ClaimsIdentity(new[] + { + new Claim(FusionClaimsTypes.AzureUniquePersonId, $"{userProfile.AzureUniqueId}"), + new Claim(ClaimTypes.Name, $"{userProfile.Name}"), + new Claim("name", $"{userProfile.Name}"), + new Claim("unique_name", userProfile.UPN), + new Claim("upn", userProfile.UPN) + }, "emulator"); + + var user = new ClaimsPrincipal(identity); + + // Need to remove this, might affect rest of the request. + // The claimstransformer will just add the profile here, which will cause an exception when the key is already added. + // Not much is using this item, so should be rather safe. + // TODO: Deprecate this in the integration lib. + httpContext.Items.Remove("FusionProfile"); + + await claimsTransformer.TransformAsync(user); + + return user; + } + } + +} + diff --git a/src/backend/api/Fusion.Resources.Api/Controllers/Person/PersonAbsenceController.cs b/src/backend/api/Fusion.Resources.Api/Controllers/Person/PersonAbsenceController.cs index f1689d8b9d..339e0a4452 100644 --- a/src/backend/api/Fusion.Resources.Api/Controllers/Person/PersonAbsenceController.cs +++ b/src/backend/api/Fusion.Resources.Api/Controllers/Person/PersonAbsenceController.cs @@ -62,14 +62,14 @@ public async Task>> GetPersonAbsenc { if (!String.IsNullOrEmpty(profile.FullDepartment)) { - or.BeResourceOwner(new DepartmentPath(profile.FullDepartment).Parent(), includeParents: false, includeDescendants: true); + or.BeResourceOwnerForDepartment(new DepartmentPath(profile.FullDepartment).Parent(), includeParents: false, includeDescendants: true); or.HaveOrgUnitScopedRole(DepartmentId.FromFullPath(profile.FullDepartment), AccessRoles.ResourceOwner); } }); r.LimitedAccessWhen(x => { if (!String.IsNullOrEmpty(profile.FullDepartment)) - x.BeResourceOwner(new DepartmentPath(profile.FullDepartment).GoToLevel(2), includeParents: false, includeDescendants: true); + x.BeResourceOwnerForDepartment(new DepartmentPath(profile.FullDepartment).GoToLevel(2), includeParents: false, includeDescendants: true); }); }); @@ -145,7 +145,7 @@ public async Task>> GetPerso // If the user is the manager - regardless if ext. hire or employee... if (!String.IsNullOrEmpty(profile.FullDepartment)) { - or.BeResourceOwner(new DepartmentPath(profile.FullDepartment).Parent(), includeParents: false, includeDescendants: true); + or.BeResourceOwnerForDepartment(new DepartmentPath(profile.FullDepartment).Parent(), includeParents: false, includeDescendants: true); or.HaveOrgUnitScopedRole(DepartmentId.FromFullPath(profile.FullDepartment), AccessRoles.ResourceOwner); } @@ -194,14 +194,14 @@ public async Task> GetPersonAbsence([FromRoute] s { if (!String.IsNullOrEmpty(profile.FullDepartment)) { - or.BeResourceOwner(new DepartmentPath(profile.FullDepartment).Parent(), includeParents: false, includeDescendants: true); + or.BeResourceOwnerForDepartment(new DepartmentPath(profile.FullDepartment).Parent(), includeParents: false, includeDescendants: true); or.HaveOrgUnitScopedRole(DepartmentId.FromFullPath(profile.FullDepartment), AccessRoles.ResourceOwner); } }); r.LimitedAccessWhen(x => { if (!String.IsNullOrEmpty(profile.FullDepartment)) - x.BeResourceOwner(new DepartmentPath(profile.FullDepartment).GoToLevel(2), includeParents: false, includeDescendants: true); + x.BeResourceOwnerForDepartment(new DepartmentPath(profile.FullDepartment).GoToLevel(2), includeParents: false, includeDescendants: true); }); }); if (authResult.Unauthorized) @@ -241,7 +241,7 @@ public async Task> CreatePersonAbsence([FromRoute { if (!String.IsNullOrEmpty(profile.FullDepartment)) { - or.BeResourceOwner(new DepartmentPath(profile.FullDepartment).Parent(), includeParents: false, includeDescendants: true); + or.BeResourceOwnerForDepartment(new DepartmentPath(profile.FullDepartment).Parent(), includeParents: false, includeDescendants: true); or.HaveOrgUnitScopedRole(DepartmentId.FromFullPath(profile.FullDepartment), AccessRoles.ResourceOwner); } }); @@ -295,7 +295,7 @@ public async Task> UpdatePersonAbsence([FromRoute { if (!String.IsNullOrEmpty(profile.FullDepartment)) { - or.BeResourceOwner(new DepartmentPath(profile.FullDepartment).Parent(), includeParents: false, includeDescendants: true); + or.BeResourceOwnerForDepartment(new DepartmentPath(profile.FullDepartment).Parent(), includeParents: false, includeDescendants: true); or.HaveOrgUnitScopedRole(DepartmentId.FromFullPath(profile.FullDepartment), AccessRoles.ResourceOwner); } }); @@ -342,7 +342,7 @@ public async Task DeletePersonAbsence([FromRoute] string personId, { if (!String.IsNullOrEmpty(profile.FullDepartment)) { - or.BeResourceOwner(new DepartmentPath(profile.FullDepartment).Parent(), includeParents: false, includeDescendants: true); + or.BeResourceOwnerForDepartment(new DepartmentPath(profile.FullDepartment).Parent(), includeParents: false, includeDescendants: true); or.HaveOrgUnitScopedRole(DepartmentId.FromFullPath(profile.FullDepartment), AccessRoles.ResourceOwner); } }); @@ -359,6 +359,7 @@ public async Task DeletePersonAbsence([FromRoute] string personId, return NoContent(); } + [EmulatedUserSupport] [HttpOptions("/persons/{personId}/absence")] public async Task GetOptionsForPerson(string personId) { @@ -380,7 +381,7 @@ public async Task GetOptionsForPerson(string personId) { if (!String.IsNullOrEmpty(profile.FullDepartment)) { - or.BeResourceOwner(new DepartmentPath(profile.FullDepartment).GoToLevel(2), includeParents: false, includeDescendants: true); + or.BeResourceOwnerForDepartment(new DepartmentPath(profile.FullDepartment).GoToLevel(2), includeParents: false, includeDescendants: true); or.HaveOrgUnitScopedRole(DepartmentId.FromFullPath(profile.FullDepartment), AccessRoles.ResourceOwner); // Employees have limited read access. @@ -399,7 +400,7 @@ public async Task GetOptionsForPerson(string personId) { if (!String.IsNullOrEmpty(profile.FullDepartment)) { - or.BeResourceOwner(new DepartmentPath(profile.FullDepartment).Parent(), includeParents: false, includeDescendants: true); + or.BeResourceOwnerForDepartment(new DepartmentPath(profile.FullDepartment).Parent(), includeParents: false, includeDescendants: true); or.HaveOrgUnitScopedRole(DepartmentId.FromFullPath(profile.FullDepartment), AccessRoles.ResourceOwner); } }); @@ -411,6 +412,7 @@ public async Task GetOptionsForPerson(string personId) return NoContent(); } + [EmulatedUserSupport] [HttpOptions("/persons/{personId}/absence/{absenceId}")] public async Task GetOptions(string personId, Guid absenceId) { @@ -432,7 +434,7 @@ public async Task GetOptions(string personId, Guid absenceId) { if (!String.IsNullOrEmpty(profile.FullDepartment)) { - or.BeResourceOwner(new DepartmentPath(profile.FullDepartment).GoToLevel(2), includeParents: false, includeDescendants: true); + or.BeResourceOwnerForDepartment(new DepartmentPath(profile.FullDepartment).GoToLevel(2), includeParents: false, includeDescendants: true); or.HaveOrgUnitScopedRole(DepartmentId.FromFullPath(profile.FullDepartment), AccessRoles.ResourceOwner); } }); @@ -448,7 +450,7 @@ public async Task GetOptions(string personId, Guid absenceId) { if (!String.IsNullOrEmpty(profile.FullDepartment)) { - or.BeResourceOwner(new DepartmentPath(profile.FullDepartment).Parent(), includeParents: false, includeDescendants: true); + or.BeResourceOwnerForDepartment(new DepartmentPath(profile.FullDepartment).Parent(), includeParents: false, includeDescendants: true); or.HaveOrgUnitScopedRole(DepartmentId.FromFullPath(profile.FullDepartment), AccessRoles.ResourceOwner); } }); diff --git a/src/backend/api/Fusion.Resources.Api/Controllers/Person/PersonController.cs b/src/backend/api/Fusion.Resources.Api/Controllers/Person/PersonController.cs index a1b312a52a..4753d1665b 100644 --- a/src/backend/api/Fusion.Resources.Api/Controllers/Person/PersonController.cs +++ b/src/backend/api/Fusion.Resources.Api/Controllers/Person/PersonController.cs @@ -124,13 +124,13 @@ public async Task>> GetPersonNotes(string perso r.AnyOf(or => { - or.BeResourceOwner(user.fullDepartment); + or.BeResourceOwnerForDepartment(user.fullDepartment); or.HaveOrgUnitScopedRole(DepartmentId.FromFullPath(user.fullDepartment), AccessRoles.ResourceOwner); }); // Limited access to other resource owners, only return shared notes. // Give access to all resource owners that share the same L3. - r.LimitedAccessWhen(or => or.BeResourceOwner(new DepartmentPath(user.fullDepartment).GoToLevel(3), includeParents: true, includeDescendants: true)); + r.LimitedAccessWhen(or => or.BeResourceOwnerForDepartment(new DepartmentPath(user.fullDepartment).GoToLevel(3), includeParents: true, includeDescendants: true)); }); if (authResult.Unauthorized) @@ -163,7 +163,7 @@ public async Task> UpdatePersonalNote(string personI r.AnyOf(or => { - or.BeResourceOwner(user.fullDepartment); + or.BeResourceOwnerForDepartment(user.fullDepartment); or.HaveOrgUnitScopedRole(DepartmentId.FromFullPath(user.fullDepartment), AccessRoles.ResourceOwner); }); }); @@ -202,7 +202,7 @@ public async Task> CreateNewPersonalNote(string pers r.AnyOf(or => { - or.BeResourceOwner(user.fullDepartment); + or.BeResourceOwnerForDepartment(user.fullDepartment); or.HaveOrgUnitScopedRole(DepartmentId.FromFullPath(user.fullDepartment), AccessRoles.ResourceOwner); }); }); @@ -236,7 +236,7 @@ public async Task DeletePersonalNote(string personId, Guid noteId) r.AnyOf(or => { - or.BeResourceOwner(user.fullDepartment); + or.BeResourceOwnerForDepartment(user.fullDepartment); or.HaveOrgUnitScopedRole(DepartmentId.FromFullPath(user.fullDepartment), AccessRoles.ResourceOwner); }); }); @@ -257,6 +257,7 @@ public async Task DeletePersonalNote(string personId, Guid noteId) return NoContent(); } + [EmulatedUserSupport] [HttpOptions("/persons/{personId}/resources/notes")] public async Task GetPersonNoteOptions(string personId) { @@ -271,12 +272,12 @@ public async Task GetPersonNoteOptions(string personId) r.AnyOf(or => { - or.BeResourceOwner(user.fullDepartment); + or.BeResourceOwnerForDepartment(user.fullDepartment); }); // Limited access to other resource owners, only return shared notes. // Give access to all resource owners that share the same L3. - r.LimitedAccessWhen(or => or.BeResourceOwner(new DepartmentPath(user.fullDepartment).GoToLevel(3), includeParents: true, includeDescendants: true)); + r.LimitedAccessWhen(or => or.BeResourceOwnerForDepartment(new DepartmentPath(user.fullDepartment).GoToLevel(3), includeParents: true, includeDescendants: true)); }); if (getResult.Success) diff --git a/src/backend/api/Fusion.Resources.Api/Controllers/Personnel/InternalPersonnelController.Preview.cs b/src/backend/api/Fusion.Resources.Api/Controllers/Personnel/InternalPersonnelController.Preview.cs index 0fbf2cb09c..761d739680 100644 --- a/src/backend/api/Fusion.Resources.Api/Controllers/Personnel/InternalPersonnelController.Preview.cs +++ b/src/backend/api/Fusion.Resources.Api/Controllers/Personnel/InternalPersonnelController.Preview.cs @@ -25,7 +25,7 @@ public async Task SearchPreview([FromRoute] PathProjectIdentifier? if (projectIdentifier is not null) or.OrgChartReadAccess(projectIdentifier.ProjectId); - or.BeResourceOwner(); + or.BeResourceOwnerForAnyDepartment(); }); }); diff --git a/src/backend/api/Fusion.Resources.Api/Controllers/Personnel/InternalPersonnelController.cs b/src/backend/api/Fusion.Resources.Api/Controllers/Personnel/InternalPersonnelController.cs index d04cfff948..babe5edc8f 100644 --- a/src/backend/api/Fusion.Resources.Api/Controllers/Personnel/InternalPersonnelController.cs +++ b/src/backend/api/Fusion.Resources.Api/Controllers/Personnel/InternalPersonnelController.cs @@ -26,6 +26,48 @@ public InternalPersonnelController() { } + [EmulatedUserSupport] + [MapToApiVersion("1.0")] + [MapToApiVersion("2.0")] + [HttpOptions("departments/{departmentString}/resources/personnel")] + public async Task>> OptionsDepartmentPersonnel([FromRoute] OrgUnitIdentifier departmentString) + { + + if (!departmentString.Exists) + return FusionApiError.NotFound(departmentString.OriginalIdentifier, "Department does not exist"); + + #region Authorization + + var sector = new DepartmentPath(departmentString.FullDepartment).Parent(); + var authResult = await Request.RequireAuthorizationAsync(r => + { + r.AnyOf(or => + { + or.BeTrustedApplication(); + or.FullControl(); + + or.FullControlInternal(); + or.BeResourceOwnerForDepartment(sector, includeParents: false, includeDescendants: true); + or.HaveOrgUnitScopedRole(DepartmentId.FromFullPath(departmentString.FullDepartment), AccessRoles.ResourceOwner); + // - Fusion.Resources.Department.ReadAll in any department scope upwards in line org. + }); + r.LimitedAccessWhen(x => + { + x.BeResourceOwnerForDepartment(new DepartmentPath(departmentString.FullDepartment).GoToLevel(2), includeParents: false, includeDescendants: true); + }); + }); + + #endregion + + + if (authResult.Success) + { + Response.Headers["Allow"] = authResult.LimitedAuth ? "GET,LIMITED" : "GET"; + } + + return NoContent(); + } + /// /// Get personnel for a department. /// @@ -67,13 +109,13 @@ public async Task>> GetDe or.FullControl(); or.FullControlInternal(); - or.BeResourceOwner(sector, includeParents: false, includeDescendants: true); + or.BeResourceOwnerForDepartment(sector, includeParents: false, includeDescendants: true); or.HaveOrgUnitScopedRole(DepartmentId.FromFullPath(departmentString.FullDepartment), AccessRoles.ResourceOwner); // - Fusion.Resources.Department.ReadAll in any department scope upwards in line org. }); r.LimitedAccessWhen(x => { - x.BeResourceOwner(new DepartmentPath(departmentString.FullDepartment).GoToLevel(2), includeParents: false, includeDescendants: true); + x.BeResourceOwnerForDepartment(new DepartmentPath(departmentString.FullDepartment).GoToLevel(2), includeParents: false, includeDescendants: true); }); }); @@ -144,12 +186,12 @@ public async Task>> GetSe or.FullControl(); or.FullControlInternal(); - or.BeResourceOwner(new DepartmentPath(sectorPath).Parent(), includeParents: false, includeDescendants: true); + or.BeResourceOwnerForDepartment(new DepartmentPath(sectorPath).Parent(), includeParents: false, includeDescendants: true); or.HaveOrgUnitScopedRole(DepartmentId.FromFullPath(sectorPath), AccessRoles.ResourceOwner); }); r.LimitedAccessWhen(x => { - x.BeResourceOwner(new DepartmentPath(sectorPath).GoToLevel(2), includeParents: false, includeDescendants: true); + x.BeResourceOwnerForDepartment(new DepartmentPath(sectorPath).GoToLevel(2), includeParents: false, includeDescendants: true); }); }); @@ -216,12 +258,12 @@ public async Task> GetPersonnelAllocati or.FullControlInternal(); - or.BeResourceOwner(sector, includeParents: false, includeDescendants: true); + or.BeResourceOwnerForDepartment(sector, includeParents: false, includeDescendants: true); or.HaveOrgUnitScopedRole(DepartmentId.FromFullPath(departmentString.FullDepartment), AccessRoles.ResourceOwner); }); r.LimitedAccessWhen(x => { - x.BeResourceOwner(new DepartmentPath(departmentString.FullDepartment).GoToLevel(2), includeParents: false, includeDescendants: true); + x.BeResourceOwnerForDepartment(new DepartmentPath(departmentString.FullDepartment).GoToLevel(2), includeParents: false, includeDescendants: true); }); }); @@ -263,7 +305,7 @@ public async Task ResetAllocationState([FromRoute] OrgUnitIdentifi or.FullControlInternal(); - or.BeResourceOwner(new DepartmentPath(departmentString.FullDepartment).GoToLevel(2), includeParents: false, includeDescendants: true); + or.BeResourceOwnerForDepartment(new DepartmentPath(departmentString.FullDepartment).GoToLevel(2), includeParents: false, includeDescendants: true); or.HaveOrgUnitScopedRole(DepartmentId.FromFullPath(departmentString.FullDepartment), AccessRoles.ResourceOwner); }); }); @@ -303,7 +345,7 @@ public async Task>> Searc if (projectIdentifier is not null) or.OrgChartReadAccess(projectIdentifier.ProjectId); - or.BeResourceOwner(); + or.BeResourceOwnerForAnyDepartment(); or.HaveAnyOrgUnitScopedRole(AccessRoles.ResourceOwner); }); }); diff --git a/src/backend/api/Fusion.Resources.Api/Controllers/Requests/ConversationsController.cs b/src/backend/api/Fusion.Resources.Api/Controllers/Requests/ConversationsController.cs index d360809dd8..989ad7dece 100644 --- a/src/backend/api/Fusion.Resources.Api/Controllers/Requests/ConversationsController.cs +++ b/src/backend/api/Fusion.Resources.Api/Controllers/Requests/ConversationsController.cs @@ -33,7 +33,7 @@ public async Task AddConversationMessage([FromRoute] Guid requestI { if (requestItem.AssignedDepartment is not null) { - or.BeResourceOwner( + or.BeResourceOwnerForDepartment( new DepartmentPath(requestItem.AssignedDepartment).GoToLevel(2), includeParents: false, includeDescendants: true @@ -42,7 +42,7 @@ public async Task AddConversationMessage([FromRoute] Guid requestI } else { - or.BeResourceOwner(); + or.BeResourceOwnerForAnyDepartment(); or.HaveAnyOrgUnitScopedRole(AccessRoles.ResourceOwner); } }); @@ -79,7 +79,7 @@ public async Task AddConversationMessage([FromRoute] Guid requestI { if (requestItem.AssignedDepartment is not null) { - or.BeResourceOwner( + or.BeResourceOwnerForDepartment( new DepartmentPath(requestItem.AssignedDepartment).GoToLevel(2), includeParents: false, includeDescendants: true @@ -88,7 +88,7 @@ public async Task AddConversationMessage([FromRoute] Guid requestI } else { - or.BeResourceOwner(); + or.BeResourceOwnerForAnyDepartment(); or.HaveAnyOrgUnitScopedRole(AccessRoles.ResourceOwner); } }); @@ -154,14 +154,14 @@ public async Task GetRequestConversation(Guid requestId, [FromRout { if (requestItem.AssignedDepartment is not null) { - or.BeResourceOwner( + or.BeResourceOwnerForDepartment( new DepartmentPath(requestItem.AssignedDepartment).GoToLevel(2), includeParents: false, includeDescendants: true ); or.HaveOrgUnitScopedRole(DepartmentId.FromFullPath(requestItem.AssignedDepartment), AccessRoles.ResourceOwner); } - or.BeResourceOwner(); + or.BeResourceOwnerForAnyDepartment(); or.HaveAnyOrgUnitScopedRole(AccessRoles.ResourceOwner); }); }); @@ -222,7 +222,7 @@ public async Task GetRequestConversation(Guid requestId, Guid mess and.RequireConversationForResourceOwner(conversation.Recipient); if (requestItem.AssignedDepartment is not null) { - and.BeResourceOwner( + and.BeResourceOwnerForDepartment( new DepartmentPath(requestItem.AssignedDepartment).GoToLevel(2), includeParents: false, includeDescendants: true @@ -231,7 +231,7 @@ public async Task GetRequestConversation(Guid requestId, Guid mess } else { - and.BeResourceOwner(); + and.BeResourceOwnerForAnyDepartment(); } }); }); @@ -301,7 +301,7 @@ public async Task UpdateRequestConversation(Guid requestId, Guid m and.RequireConversationForResourceOwner(conversation.Recipient); if (requestItem.AssignedDepartment is not null) { - and.BeResourceOwner( + and.BeResourceOwnerForDepartment( new DepartmentPath(requestItem.AssignedDepartment).GoToLevel(2), includeParents: false, includeDescendants: true @@ -310,7 +310,7 @@ public async Task UpdateRequestConversation(Guid requestId, Guid m } else { - and.BeResourceOwner(); + and.BeResourceOwnerForAnyDepartment(); } }); }); diff --git a/src/backend/api/Fusion.Resources.Api/Controllers/Requests/DepartmentRequestsController.cs b/src/backend/api/Fusion.Resources.Api/Controllers/Requests/DepartmentRequestsController.cs index 1337f05781..ec07232be0 100644 --- a/src/backend/api/Fusion.Resources.Api/Controllers/Requests/DepartmentRequestsController.cs +++ b/src/backend/api/Fusion.Resources.Api/Controllers/Requests/DepartmentRequestsController.cs @@ -5,8 +5,10 @@ using Fusion.Resources.Domain; using Fusion.Resources.Domain.Queries; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using System; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using System.Xml; @@ -19,6 +21,7 @@ namespace Fusion.Resources.Api.Controllers.Requests [ApiController] public class DepartmentRequestsController : ResourceControllerBase { + [HttpGet("departments/{departmentString}/resources/requests")] public async Task>> GetDepartmentRequests( [FromRoute] OrgUnitIdentifier departmentString, @@ -34,7 +37,7 @@ public async Task>> Get r.AlwaysAccessWhen().FullControl().FullControlInternal().BeTrustedApplication(); r.AnyOf(or => { - or.BeResourceOwner(new DepartmentPath(departmentString.FullDepartment).GoToLevel(2), includeDescendants: true); + or.BeResourceOwnerForDepartment(new DepartmentPath(departmentString.FullDepartment).GoToLevel(2), includeDescendants: true); or.HaveOrgUnitScopedRole(DepartmentId.FromFullPath(departmentString.FullDepartment), AccessRoles.ResourceOwner); }); }); @@ -52,6 +55,71 @@ public async Task>> Get return new ApiCollection(apiModel); } + + [EmulatedUserSupport] + [HttpOptions("/departments/{departmentString}/resources/requests")] + public async Task OptionsDepartmentRequests([FromRoute] OrgUnitIdentifier departmentString) + { + if (!departmentString.Exists) + return FusionApiError.NotFound(departmentString.OriginalIdentifier, "Could not locate department"); + + #region Authorization + + var authResult = await Request.RequireAuthorizationAsync(r => + { + r.AlwaysAccessWhen().FullControl().FullControlInternal().BeTrustedApplication(); + r.AnyOf(or => + { + or.BeResourceOwnerForDepartment(new DepartmentPath(departmentString.FullDepartment).GoToLevel(2), includeDescendants: true); + or.HaveOrgUnitScopedRole(DepartmentId.FromFullPath(departmentString.FullDepartment), AccessRoles.ResourceOwner); + }); + }); + + // Do not return 403, just dont return GET header + + #endregion + + var allowed = new List(); + + if (authResult.Success) + { + allowed.Add("GET, POST"); + } + + Response.Headers.Append("Allow", string.Join(',', allowed)); + return NoContent(); + } + + + [EmulatedUserSupport] + [HttpOptions("/departments/positions/{positionId}/requests")] + public async Task>> OptionsRequestsForPosition(Guid positionId) + { + #region Authorization + + var authResult = await Request.RequireAuthorizationAsync(r => + { + r.AlwaysAccessWhen().FullControl().FullControlInternal().BeTrustedApplication(); + r.AnyOf(or => + { + or.BeResourceOwnerForAnyDepartment(); + or.HaveAnyOrgUnitScopedRole(AccessRoles.ResourceOwner); + }); + }); + + #endregion + + var allowed = new List(); + + if (authResult.Success) + { + allowed.Add("GET"); + } + + Response.Headers.Append("Allow", string.Join(',', allowed)); + return NoContent(); + } + /// /// List all requests that is relevant for a position. This endpoint is relevant for resource owners, and will include drafts that has not been sent to the task owners. /// @@ -75,7 +143,7 @@ public async Task>> Get r.AlwaysAccessWhen().FullControl().FullControlInternal().BeTrustedApplication(); r.AnyOf(or => { - or.BeResourceOwner(); + or.BeResourceOwnerForAnyDepartment(); or.HaveAnyOrgUnitScopedRole(AccessRoles.ResourceOwner); }); }); @@ -108,7 +176,7 @@ public async Task> GetDepartmentTimeline( r.AlwaysAccessWhen().FullControl().FullControlInternal(); r.AnyOf(or => { - or.BeResourceOwner(new DepartmentPath(departmentString.FullDepartment).GoToLevel(2), includeDescendants: true); + or.BeResourceOwnerForDepartment(new DepartmentPath(departmentString.FullDepartment).GoToLevel(2), includeDescendants: true); or.HaveOrgUnitScopedRole(DepartmentId.FromFullPath(departmentString.FullDepartment), AccessRoles.ResourceOwner); }); }); @@ -158,7 +226,7 @@ public async Task>> Get r.AlwaysAccessWhen().FullControl().FullControlInternal(); r.AnyOf(or => { - or.BeResourceOwner(new DepartmentPath(departmentString.FullDepartment).GoToLevel(2), includeDescendants: true); + or.BeResourceOwnerForDepartment(new DepartmentPath(departmentString.FullDepartment).GoToLevel(2), includeDescendants: true); or.HaveOrgUnitScopedRole(DepartmentId.FromFullPath(departmentString.FullDepartment), AccessRoles.ResourceOwner); }); }); @@ -189,7 +257,7 @@ public async Task GetTBNPositions([FromRoute] OrgUnitIdentifier de r.AlwaysAccessWhen().FullControl().FullControlInternal(); r.AnyOf(or => { - or.BeResourceOwner(new DepartmentPath(departmentString.FullDepartment).GoToLevel(2), includeDescendants: true); + or.BeResourceOwnerForDepartment(new DepartmentPath(departmentString.FullDepartment).GoToLevel(2), includeDescendants: true); or.HaveOrgUnitScopedRole(DepartmentId.FromFullPath(departmentString.FullDepartment), AccessRoles.ResourceOwner); }); }); @@ -220,7 +288,7 @@ public async Task GetTbnPositionsTimeline( r.AlwaysAccessWhen().FullControl().FullControlInternal(); r.AnyOf(or => { - or.BeResourceOwner(new DepartmentPath(departmentString.FullDepartment).GoToLevel(2), includeDescendants: true); + or.BeResourceOwnerForDepartment(new DepartmentPath(departmentString.FullDepartment).GoToLevel(2), includeDescendants: true); or.HaveOrgUnitScopedRole(DepartmentId.FromFullPath(departmentString.FullDepartment), AccessRoles.ResourceOwner); }); }); diff --git a/src/backend/api/Fusion.Resources.Api/Controllers/Requests/InternalRequestsController.cs b/src/backend/api/Fusion.Resources.Api/Controllers/Requests/InternalRequestsController.cs index faa2ef0655..24048c947b 100644 --- a/src/backend/api/Fusion.Resources.Api/Controllers/Requests/InternalRequestsController.cs +++ b/src/backend/api/Fusion.Resources.Api/Controllers/Requests/InternalRequestsController.cs @@ -219,7 +219,7 @@ public async Task> CreateResourceOwne r.AlwaysAccessWhen().FullControl().FullControlInternal(); r.AnyOf(or => { - or.BeResourceOwner(new DepartmentPath(departmentString.FullDepartment).Parent(), includeParents: false, includeDescendants: true); + or.BeResourceOwnerForDepartment(new DepartmentPath(departmentString.FullDepartment).Parent(), includeParents: false, includeDescendants: true); or.HaveOrgUnitScopedRole(DepartmentId.FromFullPath(departmentString.FullDepartment), AccessRoles.ResourceOwner); }); }); @@ -414,7 +414,7 @@ public async Task> PatchInternalReque { if (item.AssignedDepartment is not null) { - or.BeResourceOwner( + or.BeResourceOwnerForDepartment( new DepartmentPath(item.AssignedDepartment).GoToLevel(2), includeParents: false, includeDescendants: true @@ -423,7 +423,7 @@ public async Task> PatchInternalReque } else { - or.BeResourceOwner(); + or.BeResourceOwnerForAnyDepartment(); or.HaveAnyOrgUnitScopedRole(AccessRoles.ResourceOwner); } }); @@ -518,7 +518,7 @@ public async Task>> Get var departmentString = filter.Value; if (!string.IsNullOrEmpty(departmentString)) { - or.BeResourceOwner( + or.BeResourceOwnerForDepartment( new DepartmentPath(departmentString).GoToLevel(2), includeParents: false, includeDescendants: true @@ -603,7 +603,7 @@ public async Task>> Get r.AlwaysAccessWhen().FullControl().FullControlInternal().BeTrustedApplication(); r.AnyOf(or => { - or.BeResourceOwner(); + or.BeResourceOwnerForAnyDepartment(); or.HaveAnyOrgUnitScopedRole(AccessRoles.ResourceOwner); }); }); @@ -690,7 +690,7 @@ public async Task> GetResourceAllocat { if (requestItem.AssignedDepartment is not null) { - or.BeResourceOwner( + or.BeResourceOwnerForDepartment( new DepartmentPath(requestItem.AssignedDepartment).GoToLevel(2), includeParents: false, includeDescendants: true @@ -699,7 +699,7 @@ public async Task> GetResourceAllocat } else { - or.BeResourceOwner(); + or.BeResourceOwnerForAnyDepartment(); or.HaveAnyOrgUnitScopedRole(AccessRoles.ResourceOwner); } }); @@ -836,7 +836,7 @@ public async Task> StartResourceOwner r.AlwaysAccessWhen().FullControl().FullControlInternal(); r.AnyOf(or => { - or.BeResourceOwner(new DepartmentPath(result.AssignedDepartment).GoToLevel(2), includeDescendants: true); + or.BeResourceOwnerForDepartment(new DepartmentPath(result.AssignedDepartment).GoToLevel(2), includeDescendants: true); or.HaveOrgUnitScopedRole(DepartmentId.FromFullPath(result.AssignedDepartment), AccessRoles.ResourceOwner); or.BeRequestCreator(requestId); }); @@ -1075,12 +1075,12 @@ public async Task ResetWorkflow([FromRoute] RequestIdentifier requ { if (!string.IsNullOrEmpty(requestItem.AssignedDepartment)) { - or.BeResourceOwner(new DepartmentPath(requestItem.AssignedDepartment).Parent(), includeParents: false, includeDescendants: true); + or.BeResourceOwnerForDepartment(new DepartmentPath(requestItem.AssignedDepartment).Parent(), includeParents: false, includeDescendants: true); or.HaveOrgUnitScopedRole(DepartmentId.FromFullPath(requestItem.AssignedDepartment), AccessRoles.ResourceOwner); } else { - or.BeResourceOwner(); + or.BeResourceOwnerForAnyDepartment(); or.HaveAnyOrgUnitScopedRole(AccessRoles.ResourceOwner); } }); @@ -1097,6 +1097,7 @@ public async Task ResetWorkflow([FromRoute] RequestIdentifier requ #region Comments + [EmulatedUserSupport] [HttpOptions("/resources/requests/internal/{requestId}/comments")] public async Task GetCommentOptions([FromRoute] RequestIdentifier requestId) { @@ -1120,7 +1121,7 @@ public async Task GetCommentOptions([FromRoute] RequestIdentifier r.AlwaysAccessWhen().FullControl().FullControlInternal(); r.AnyOf(or => { - or.BeResourceOwner(new DepartmentPath(requiredDepartment).Parent(), includeParents: true, includeDescendants: true); + or.BeResourceOwnerForDepartment(new DepartmentPath(requiredDepartment).Parent(), includeParents: true, includeDescendants: true); or.HaveOrgUnitScopedRole(DepartmentId.FromFullPath(requiredDepartment), AccessRoles.ResourceOwner); }); }); @@ -1139,6 +1140,7 @@ public async Task GetCommentOptions([FromRoute] RequestIdentifier return NoContent(); } + [EmulatedUserSupport] [HttpOptions("/resources/requests/internal/{requestId}/comments/{commentId}")] public async Task GetCommentOptions([FromRoute] RequestIdentifier requestId, Guid commentId) { @@ -1165,7 +1167,7 @@ public async Task GetCommentOptions([FromRoute] RequestIdentifier r.AlwaysAccessWhen().FullControl().FullControlInternal(); r.AnyOf(or => { - or.BeResourceOwner(new DepartmentPath(requiredDepartment).Parent(), includeParents: true, includeDescendants: true); + or.BeResourceOwnerForDepartment(new DepartmentPath(requiredDepartment).Parent(), includeParents: true, includeDescendants: true); or.HaveOrgUnitScopedRole(DepartmentId.FromFullPath(requiredDepartment), AccessRoles.ResourceOwner); }); }); @@ -1208,7 +1210,7 @@ public async Task> AddRequestComment([FromRoute] r.AlwaysAccessWhen().FullControl().FullControlInternal(); r.AnyOf(or => { - or.BeResourceOwner(new DepartmentPath(requiredDepartment).Parent(), includeParents: true, includeDescendants: true); + or.BeResourceOwnerForDepartment(new DepartmentPath(requiredDepartment).Parent(), includeParents: true, includeDescendants: true); or.HaveOrgUnitScopedRole(DepartmentId.FromFullPath(requiredDepartment), AccessRoles.ResourceOwner); }); }); @@ -1249,7 +1251,7 @@ public async Task>> GetRequestCommen r.AlwaysAccessWhen().FullControl().FullControlInternal(); r.AnyOf(or => { - or.BeResourceOwner(new DepartmentPath(requiredDepartment).Parent(), includeParents: true, includeDescendants: true); + or.BeResourceOwnerForDepartment(new DepartmentPath(requiredDepartment).Parent(), includeParents: true, includeDescendants: true); or.HaveOrgUnitScopedRole(DepartmentId.FromFullPath(requiredDepartment), AccessRoles.ResourceOwner); }); }); @@ -1293,7 +1295,7 @@ public async Task> GetRequestComment([FromRoute] r.AlwaysAccessWhen().FullControl().FullControlInternal(); r.AnyOf(or => { - or.BeResourceOwner(new DepartmentPath(requiredDepartment).Parent(), includeParents: true, includeDescendants: true); + or.BeResourceOwnerForDepartment(new DepartmentPath(requiredDepartment).Parent(), includeParents: true, includeDescendants: true); or.HaveOrgUnitScopedRole(DepartmentId.FromFullPath(requiredDepartment), AccessRoles.ResourceOwner); }); }); @@ -1336,7 +1338,7 @@ public async Task> UpdateRequestComment([FromRou r.AlwaysAccessWhen().FullControl().FullControlInternal(); r.AnyOf(or => { - or.BeResourceOwner(new DepartmentPath(requiredDepartment).Parent(), includeParents: true, includeDescendants: true); + or.BeResourceOwnerForDepartment(new DepartmentPath(requiredDepartment).Parent(), includeParents: true, includeDescendants: true); or.HaveOrgUnitScopedRole(DepartmentId.FromFullPath(requiredDepartment), AccessRoles.ResourceOwner); }); }); @@ -1379,7 +1381,7 @@ public async Task DeleteRequestComment([FromRoute] RequestIdentifi r.AlwaysAccessWhen().FullControl().FullControlInternal(); r.AnyOf(or => { - or.BeResourceOwner(new DepartmentPath(requiredDepartment).Parent(), includeParents: true, includeDescendants: true); + or.BeResourceOwnerForDepartment(new DepartmentPath(requiredDepartment).Parent(), includeParents: true, includeDescendants: true); or.HaveOrgUnitScopedRole(DepartmentId.FromFullPath(requiredDepartment), AccessRoles.ResourceOwner); }); }); @@ -1396,6 +1398,7 @@ public async Task DeleteRequestComment([FromRoute] RequestIdentifi #endregion Comments + [EmulatedUserSupport] [HttpOptions("/projects/{projectIdentifier}/requests/{requestId}/approve")] [HttpOptions("/projects/{projectIdentifier}/resources/requests/{requestId}/approve")] public async Task> CheckApprovalAccess([FromRoute] PathProjectIdentifier projectIdentifier, [FromRoute] RequestIdentifier requestId) @@ -1425,6 +1428,7 @@ public async Task> CheckApprovalAcces return NoContent(); } + [EmulatedUserSupport] [HttpOptions("/projects/{projectIdentifier}/requests/{requestId}")] [HttpOptions("/projects/{projectIdentifier}/resources/requests/{requestId}")] public async Task CheckProjectAllocationRequestAccess([FromRoute] PathProjectIdentifier projectIdentifier, [FromRoute] RequestIdentifier requestId) @@ -1448,7 +1452,7 @@ public async Task CheckProjectAllocationRequestAccess([FromRoute] if (item.AssignedDepartment is not null) { - or.BeResourceOwner( + or.BeResourceOwnerForDepartment( new DepartmentPath(item.AssignedDepartment).GoToLevel(2), includeParents: false, includeDescendants: true @@ -1456,7 +1460,7 @@ public async Task CheckProjectAllocationRequestAccess([FromRoute] } else { - or.BeResourceOwner(); + or.BeResourceOwnerForAnyDepartment(); or.HaveAnyOrgUnitScopedRole(AccessRoles.ResourceOwner); } @@ -1496,7 +1500,7 @@ public async Task CheckProjectAllocationRequestAccess([FromRoute] if (item.AssignedDepartment is not null) { - or.BeResourceOwner( + or.BeResourceOwnerForDepartment( new DepartmentPath(item.AssignedDepartment).GoToLevel(2), includeParents: false, includeDescendants: true @@ -1504,7 +1508,7 @@ public async Task CheckProjectAllocationRequestAccess([FromRoute] } else { - or.BeResourceOwner(); + or.BeResourceOwnerForAnyDepartment(); or.HaveAnyOrgUnitScopedRole(AccessRoles.ResourceOwner); } }); @@ -1525,6 +1529,7 @@ public async Task CheckProjectAllocationRequestAccess([FromRoute] /// Instance / allocation to target /// The request type to create /// + [EmulatedUserSupport] [HttpOptions("/projects/{projectIdentifier}/positions/{positionId}/instances/{instanceId}/resources/requests")] public async Task CheckInstanceRequestTypeAsync([FromRoute] PathProjectIdentifier projectIdentifier, Guid positionId, Guid instanceId, [FromQuery] string? requestType) { @@ -1553,6 +1558,7 @@ public async Task CheckInstanceRequestTypeAsync([FromRoute] PathPr return NoContent(); } + [EmulatedUserSupport] [HttpOptions("/departments/{departmentString}/resources/requests/{requestId}")] public async Task CheckDepartmentRequestAccess([FromRoute] OrgUnitIdentifier departmentString, [FromRoute] RequestIdentifier requestId) { @@ -1579,7 +1585,7 @@ public async Task CheckDepartmentRequestAccess([FromRoute] OrgUnit if (item.AssignedDepartment is not null) { - or.BeResourceOwner( + or.BeResourceOwnerForDepartment( new DepartmentPath(item.AssignedDepartment).GoToLevel(2), includeParents: false, includeDescendants: true @@ -1588,7 +1594,7 @@ public async Task CheckDepartmentRequestAccess([FromRoute] OrgUnit } else { - or.BeResourceOwner(); + or.BeResourceOwnerForAnyDepartment(); or.HaveAnyOrgUnitScopedRole(AccessRoles.ResourceOwner); } }); @@ -1621,7 +1627,7 @@ public async Task CheckDepartmentRequestAccess([FromRoute] OrgUnit if (item.AssignedDepartment is not null) { - or.BeResourceOwner( + or.BeResourceOwnerForDepartment( new DepartmentPath(item.AssignedDepartment).GoToLevel(2), includeParents: false, includeDescendants: true @@ -1630,7 +1636,7 @@ public async Task CheckDepartmentRequestAccess([FromRoute] OrgUnit } else { - or.BeResourceOwner(); + or.BeResourceOwnerForAnyDepartment(); or.HaveAnyOrgUnitScopedRole(AccessRoles.ResourceOwner); } or.BeRequestCreator(requestId); @@ -1642,28 +1648,13 @@ public async Task CheckDepartmentRequestAccess([FromRoute] OrgUnit return NoContent(); } + [EmulatedUserSupport] [HttpOptions("/projects/{projectIdentifier}/requests")] [HttpOptions("/projects/{projectIdentifier}/resources/requests")] - [HttpOptions("/departments/{departmentString}/resources/requests")] - public async Task>> GetResourceAllocationRequestsOptions( - [FromRoute] PathProjectIdentifier projectIdentifier, [FromRoute] OrgUnitIdentifier? departmentString) + public async Task>> GetResourceAllocationRequestsOptions([FromRoute] PathProjectIdentifier projectIdentifier) { var allowedVerbs = new List(); - var postAuth = await Request.RequireAuthorizationAsync(r => - { - r.AlwaysAccessWhen().FullControl().FullControlInternal(); - r.AnyOf(or => - { - if (departmentString is not null) - { - or.BeResourceOwner(departmentString.FullDepartment, includeParents: false, includeDescendants: true); - or.HaveOrgUnitScopedRole(DepartmentId.FromFullPath(departmentString.FullDepartment), AccessRoles.ResourceOwner); - } - }); - }); - if (postAuth.Success) allowedVerbs.Add("POST"); - var getAuth = await Request.RequireAuthorizationAsync(r => { r.AlwaysAccessWhen() @@ -1675,8 +1666,6 @@ public async Task>> Get { // For now everyone with a position in the project can view requests or.HaveOrgchartPosition(ProjectOrganisationIdentifier.FromOrgChartId(projectIdentifier.ProjectId)); - if (departmentString is not null) - or.BeResourceOwner(departmentString.FullDepartment, includeParents: false, includeDescendants: true); }); }); @@ -1687,6 +1676,7 @@ public async Task>> Get return NoContent(); } + [EmulatedUserSupport] [HttpOptions("/departments/{departmentPath}/resources/requests/{requestId}/approve")] public async Task GetWorkflowApprovalOptions([FromRoute] RequestIdentifier requestId) { diff --git a/src/backend/api/Fusion.Resources.Api/Controllers/Requests/RequestActionsController.cs b/src/backend/api/Fusion.Resources.Api/Controllers/Requests/RequestActionsController.cs index 23fde543b3..4c482f1531 100644 --- a/src/backend/api/Fusion.Resources.Api/Controllers/Requests/RequestActionsController.cs +++ b/src/backend/api/Fusion.Resources.Api/Controllers/Requests/RequestActionsController.cs @@ -39,7 +39,7 @@ public async Task AddRequestActionAsync([FromRoute] Guid requestId if (requestItem.AssignedDepartment is not null) { - or.BeResourceOwner( + or.BeResourceOwnerForDepartment( new DepartmentPath(requestItem.AssignedDepartment).GoToLevel(2), includeParents: false, includeDescendants: true @@ -48,7 +48,7 @@ public async Task AddRequestActionAsync([FromRoute] Guid requestId } else { - or.BeResourceOwner(); + or.BeResourceOwnerForAnyDepartment(); or.HaveAnyOrgUnitScopedRole(AccessRoles.ResourceOwner); } @@ -106,7 +106,7 @@ public async Task GetRequestActions([FromRoute] Guid requestId, [F { if (request.AssignedDepartment is not null) { - or.BeResourceOwner( + or.BeResourceOwnerForDepartment( new DepartmentPath(request.AssignedDepartment).GoToLevel(2), includeParents: false, includeDescendants: true @@ -115,7 +115,7 @@ public async Task GetRequestActions([FromRoute] Guid requestId, [F } else { - or.BeResourceOwner(); + or.BeResourceOwnerForAnyDepartment(); or.HaveAnyOrgUnitScopedRole(AccessRoles.ResourceOwner); } }); @@ -163,7 +163,7 @@ public async Task GetRequestAction([FromRoute] Guid requestId, [Fr if (request.AssignedDepartment is not null) { - or.BeResourceOwner( + or.BeResourceOwnerForDepartment( new DepartmentPath(request.AssignedDepartment).GoToLevel(2), includeParents: false, includeDescendants: true @@ -172,7 +172,7 @@ public async Task GetRequestAction([FromRoute] Guid requestId, [Fr } else { - or.BeResourceOwner(); + or.BeResourceOwnerForAnyDepartment(); or.HaveAnyOrgUnitScopedRole(AccessRoles.ResourceOwner); } }); @@ -210,7 +210,7 @@ public async Task UpdateRequestAction([FromRoute] Guid requestId, { if (request.AssignedDepartment is not null) { - or.BeResourceOwner( + or.BeResourceOwnerForDepartment( new DepartmentPath(request.AssignedDepartment).GoToLevel(2), includeParents: false, includeDescendants: true @@ -219,7 +219,7 @@ public async Task UpdateRequestAction([FromRoute] Guid requestId, } else { - or.BeResourceOwner(); + or.BeResourceOwnerForAnyDepartment(); or.HaveAnyOrgUnitScopedRole(AccessRoles.ResourceOwner); } } @@ -283,7 +283,7 @@ public async Task DeleteRequestAction([FromRoute] Guid requestId, { if (request.AssignedDepartment is not null) { - or.BeResourceOwner( + or.BeResourceOwnerForDepartment( new DepartmentPath(request.AssignedDepartment).GoToLevel(2), includeParents: false, includeDescendants: true @@ -292,7 +292,7 @@ public async Task DeleteRequestAction([FromRoute] Guid requestId, } else { - or.BeResourceOwner(); + or.BeResourceOwnerForAnyDepartment(); or.HaveAnyOrgUnitScopedRole(AccessRoles.ResourceOwner); } } diff --git a/src/backend/api/Fusion.Resources.Api/Controllers/Requests/SecondOpinionController.cs b/src/backend/api/Fusion.Resources.Api/Controllers/Requests/SecondOpinionController.cs index 91e3ee21b5..ad1c54d99e 100644 --- a/src/backend/api/Fusion.Resources.Api/Controllers/Requests/SecondOpinionController.cs +++ b/src/backend/api/Fusion.Resources.Api/Controllers/Requests/SecondOpinionController.cs @@ -15,6 +15,7 @@ namespace Fusion.Resources.Api.Controllers.Requests [ApiController] public class SecondOpinionController : ResourceControllerBase { + [EmulatedUserSupport] [HttpOptions("/resources/requests/internal/{requestId}/second-opinions")] public async Task CheckSecondOpinionAccess(Guid requestId) { @@ -30,7 +31,7 @@ public async Task CheckSecondOpinionAccess(Guid requestId) { if (requestItem.AssignedDepartment is not null) { - or.BeResourceOwner( + or.BeResourceOwnerForDepartment( new DepartmentPath(requestItem.AssignedDepartment).GoToLevel(2), includeParents: false, includeDescendants: true @@ -38,7 +39,7 @@ public async Task CheckSecondOpinionAccess(Guid requestId) } else { - or.BeResourceOwner(); + or.BeResourceOwnerForAnyDepartment(); or.HaveAnyOrgUnitScopedRole(AccessRoles.ResourceOwner); } }); @@ -75,7 +76,7 @@ public async Task> RequestSecondOpinion(Guid requ { if (requestItem.AssignedDepartment is not null) { - or.BeResourceOwner( + or.BeResourceOwnerForDepartment( new DepartmentPath(requestItem.AssignedDepartment).GoToLevel(2), includeParents: false, includeDescendants: true @@ -83,7 +84,7 @@ public async Task> RequestSecondOpinion(Guid requ } else { - or.BeResourceOwner(); + or.BeResourceOwnerForAnyDepartment(); or.HaveAnyOrgUnitScopedRole(AccessRoles.ResourceOwner); } }); @@ -121,7 +122,7 @@ public async Task>> GetSecondOpinions(Guid r { if (requestItem.AssignedDepartment is not null) { - or.BeResourceOwner( + or.BeResourceOwnerForDepartment( new DepartmentPath(requestItem.AssignedDepartment).GoToLevel(2), includeParents: false, includeDescendants: true @@ -129,7 +130,7 @@ public async Task>> GetSecondOpinions(Guid r } else { - or.BeResourceOwner(); + or.BeResourceOwnerForAnyDepartment(); or.HaveAnyOrgUnitScopedRole(AccessRoles.ResourceOwner); } or.HaveBasicRead(requestId); @@ -148,7 +149,7 @@ public async Task>> GetSecondOpinions(Guid r return Ok(new ApiSecondOpinionResult(result, User.GetAzureUniqueIdOrThrow())); } - + [EmulatedUserSupport] [HttpOptions("/resources/requests/internal/{requestId}/second-opinions/{secondOpinionId}")] public async Task CheckSecondOpinionAccess(Guid requestId, Guid secondOpinionId) { @@ -273,6 +274,7 @@ public async Task> PatchSecondOpinion(Guid reques return Ok(new ApiSecondOpinion(secondOpinion!, User.GetAzureUniqueIdOrThrow())); } + [EmulatedUserSupport] [HttpOptions("/resources/requests/internal/{requestId}/second-opinions/{secondOpinionId}/responses/{responseId}")] public async Task> CheckPatchSecondOpinionResponse(Guid requestId, Guid secondOpinionId, Guid responseId) { @@ -388,6 +390,7 @@ public async Task> DeleteSecondOpinionRes return NoContent(); } + [EmulatedUserSupport] [HttpOptions("/persons/{personId}/second-opinions/")] [HttpOptions("/persons/{personId}/second-opinions/responses")] public async Task CheckPersonalAccess(string personId) diff --git a/src/backend/api/Fusion.Resources.Api/Controllers/Requests/ShareRequestsController.cs b/src/backend/api/Fusion.Resources.Api/Controllers/Requests/ShareRequestsController.cs index 955acd7b54..d9847221d8 100644 --- a/src/backend/api/Fusion.Resources.Api/Controllers/Requests/ShareRequestsController.cs +++ b/src/backend/api/Fusion.Resources.Api/Controllers/Requests/ShareRequestsController.cs @@ -31,7 +31,7 @@ public async Task ShareRequest(Guid requestId, ShareRequestReques { if (requestItem.AssignedDepartment is not null) { - or.BeResourceOwner( + or.BeResourceOwnerForDepartment( new DepartmentPath(requestItem.AssignedDepartment).GoToLevel(2), includeParents: false, includeDescendants: true @@ -39,7 +39,7 @@ public async Task ShareRequest(Guid requestId, ShareRequestReques } else { - or.BeResourceOwner(); + or.BeResourceOwnerForAnyDepartment(); or.HaveAnyOrgUnitScopedRole(AccessRoles.ResourceOwner); } }); @@ -111,7 +111,7 @@ public async Task DeleteSharedRequests(Guid requestId, string sha { if (requestItem.AssignedDepartment is not null) { - or.BeResourceOwner( + or.BeResourceOwnerForDepartment( new DepartmentPath(requestItem.AssignedDepartment).GoToLevel(2), includeParents: false, includeDescendants: true @@ -119,7 +119,7 @@ public async Task DeleteSharedRequests(Guid requestId, string sha } else { - or.BeResourceOwner(); + or.BeResourceOwnerForAnyDepartment(); or.HaveAnyOrgUnitScopedRole(AccessRoles.ResourceOwner); } }); diff --git a/src/backend/api/Fusion.Resources.Api/Controllers/ResponsibilityMatrix/ResponsibilityMatrixController.cs b/src/backend/api/Fusion.Resources.Api/Controllers/ResponsibilityMatrix/ResponsibilityMatrixController.cs index 1f64c788c1..607f6688f8 100644 --- a/src/backend/api/Fusion.Resources.Api/Controllers/ResponsibilityMatrix/ResponsibilityMatrixController.cs +++ b/src/backend/api/Fusion.Resources.Api/Controllers/ResponsibilityMatrix/ResponsibilityMatrixController.cs @@ -29,7 +29,7 @@ public async Task>> GetRespo or.ScopeAccess(ScopeAccess.ManageMatrices); }); - r.LimitedAccessWhen(l => l.BeResourceOwner()); + r.LimitedAccessWhen(l => l.BeResourceOwnerForAnyDepartment()); }); if (authResult.Unauthorized) @@ -44,7 +44,7 @@ public async Task>> GetRespo // filter items if limited auth if (authResult.LimitedAuth) { - var resourceOwnerDepartments = User.GetResponsibleForDepartments() + var resourceOwnerDepartments = User.GetManagerForDepartments() .Select(d => new DepartmentPath(d)) .ToList(); @@ -77,7 +77,7 @@ public async Task> GetResponsibilityMatrix { var department = new DepartmentPath(responsibilityMatrix.Unit); - or.BeResourceOwner(responsibilityMatrix.Unit, includeParents: true); + or.BeResourceOwnerForDepartment(responsibilityMatrix.Unit, includeParents: true); or.BeSiblingResourceOwner(department); or.BeDirectChildResourceOwner(department); } @@ -109,7 +109,7 @@ public async Task> CreateResponsibilityMat r.AnyOf(or => { or.ScopeAccess(ScopeAccess.ManageMatrices); - or.BeResourceOwner(request.Unit, includeParents: true); + or.BeResourceOwnerForDepartment(request.Unit, includeParents: true); if (request.Unit is not null) { var department = new DepartmentPath(request.Unit); @@ -158,11 +158,12 @@ public async Task> UpdateResponsibilityMat { or.ScopeAccess(ScopeAccess.ManageMatrices); - or.BeResourceOwner(request.Unit, includeParents: true); + or.BeResourceOwnerForDepartment(request.Unit, includeParents: true); if (request.Unit is not null) { var department = new DepartmentPath(request.Unit); + // TODO: Refactor this or.BeSiblingResourceOwner(department); or.BeDirectChildResourceOwner(department); } @@ -209,7 +210,7 @@ public async Task DeleteResponsibilityMatrix(Guid matrixId) { var department = new DepartmentPath(responsibilityMatrix.Unit); - or.BeResourceOwner(responsibilityMatrix.Unit, includeParents: true); + or.BeResourceOwnerForDepartment(responsibilityMatrix.Unit, includeParents: true); or.BeSiblingResourceOwner(department); or.BeDirectChildResourceOwner(department); } diff --git a/src/backend/api/Fusion.Resources.Api/Deployment/k8s/pr-deployment-env.yml b/src/backend/api/Fusion.Resources.Api/Deployment/k8s/pr-deployment-env.yml index 68c3d142ab..21fdb0362f 100644 --- a/src/backend/api/Fusion.Resources.Api/Deployment/k8s/pr-deployment-env.yml +++ b/src/backend/api/Fusion.Resources.Api/Deployment/k8s/pr-deployment-env.yml @@ -24,6 +24,8 @@ metadata: labels: environment: pr prNumber: '{{prNumber}}' + annotations: + k8s-ttl-controller.twin.sh/ttl: '3d' spec: replicas: 1 strategy: @@ -95,6 +97,8 @@ metadata: labels: environment: pr prNumber: '{{prNumber}}' + annotations: + k8s-ttl-controller.twin.sh/ttl: '3d' spec: selector: @@ -112,6 +116,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" diff --git a/src/backend/api/Fusion.Resources.Api/Extensions/ClaimsPrincipalExtensions.cs b/src/backend/api/Fusion.Resources.Api/Extensions/ClaimsPrincipalExtensions.cs new file mode 100644 index 0000000000..c364d0c05c --- /dev/null +++ b/src/backend/api/Fusion.Resources.Api/Extensions/ClaimsPrincipalExtensions.cs @@ -0,0 +1,16 @@ +using Fusion.Integration; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; + +namespace Fusion.Resources.Api +{ + public static class ClaimsPrincipalExtensions + { + /// + /// Workday version. Use this instead of User.GetResponsibleForDepartments() in integration lib. + /// + public static IEnumerable GetManagerForDepartments(this ClaimsPrincipal user) => user.FindAll(ResourcesClaimTypes.ResourceOwnerForDepartment) + .Select(c => c.Value); + } +} diff --git a/src/backend/api/Fusion.Resources.Api/Fusion.Resources.Api.csproj b/src/backend/api/Fusion.Resources.Api/Fusion.Resources.Api.csproj index bcf86157b9..908c1bdfa0 100644 --- a/src/backend/api/Fusion.Resources.Api/Fusion.Resources.Api.csproj +++ b/src/backend/api/Fusion.Resources.Api/Fusion.Resources.Api.csproj @@ -17,23 +17,23 @@ - - - - - - - - - - - + + + + + + + + + + + - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -42,7 +42,7 @@ - + diff --git a/src/backend/api/Fusion.Resources.Logic/Requests/Authorization/CanApproveStepHandlerBase.cs b/src/backend/api/Fusion.Resources.Logic/Requests/Authorization/CanApproveStepHandlerBase.cs index fc37e0ccaa..9e3a6cb7fb 100644 --- a/src/backend/api/Fusion.Resources.Logic/Requests/Authorization/CanApproveStepHandlerBase.cs +++ b/src/backend/api/Fusion.Resources.Logic/Requests/Authorization/CanApproveStepHandlerBase.cs @@ -6,11 +6,9 @@ using Fusion.Resources.Database.Entities; using Fusion.Resources.Domain; using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Authorization.Infrastructure; using Microsoft.AspNetCore.Http; using System; using System.Linq; -using System.Security.Claims; using System.Text; using System.Threading.Tasks; @@ -36,8 +34,8 @@ protected async Task CheckAccess(DbResourceAllocationRequest request, WorkflowAc builder.AlwaysAccessWhen(or => { or.BeTrustedApplication(); - or.AddRule(new AssertionRequirement(ctx => ctx.User.IsInRole("Fusion.Resources.FullControl"))); - or.AddRule(new AssertionRequirement(ctx => ctx.User.IsInRole("Fusion.Resources.Internal.FullControl"))); + or.GlobalRoleAccess("Fusion.Resources.FullControl"); + or.GlobalRoleAccess("Fusion.Resources.Internal.FullControl"); }); builder.AnyOf(or => @@ -47,17 +45,17 @@ protected async Task CheckAccess(DbResourceAllocationRequest request, WorkflowAc var path = new DepartmentPath(request.AssignedDepartment); if (row.IsAllResourceOwnersAllowed) - or.BeResourceOwner(path.GoToLevel(2), includeDescendants: true); + or.BeResourceOwnerForDepartment(path.GoToLevel(2), includeDescendants: true); if (row.IsParentResourceOwnerAllowed) - or.BeResourceOwner(path.Parent(), includeDescendants: false); + or.BeResourceOwnerForDepartment(path.Parent(), includeDescendants: false); if (row.IsSiblingResourceOwnerAllowed) - or.BeResourceOwner(path.Parent(), includeDescendants: true); + or.BeResourceOwnerForDepartment(path.Parent(), includeDescendants: true); if (row.IsResourceOwnerAllowed) { - or.BeResourceOwner(request.AssignedDepartment, includeDescendants: false); + or.BeResourceOwnerForDepartment(request.AssignedDepartment, includeDescendants: false); or.HaveOrgUnitScopedRole(DepartmentId.FromFullPath(request.AssignedDepartment), AccessRoles.ResourceOwner); } } diff --git a/src/backend/tests/Fusion.Resources.Api.Tests/AuthorizationTests/SecurityMatrixTests.cs b/src/backend/tests/Fusion.Resources.Api.Tests/AuthorizationTests/SecurityMatrixTests.cs index f9b56dd75f..6587cf115f 100644 --- a/src/backend/tests/Fusion.Resources.Api.Tests/AuthorizationTests/SecurityMatrixTests.cs +++ b/src/backend/tests/Fusion.Resources.Api.Tests/AuthorizationTests/SecurityMatrixTests.cs @@ -1,4 +1,6 @@ -using Fusion.ApiClients.Org; +using Azure.Core; +using FluentAssertions; +using Fusion.ApiClients.Org; using Fusion.Integration.Profile; using Fusion.Integration.Profile.ApiClient; using Fusion.Resources.Api.Controllers; @@ -11,6 +13,7 @@ using Fusion.Testing.Mocks; using Fusion.Testing.Mocks.OrgService; using Fusion.Testing.Mocks.ProfileService; +using Moq; using System; using System.Collections.Generic; using System.Net; @@ -43,6 +46,9 @@ public class SecurityMatrixTests : IClassFixture, IAsyncLife private OrgRequestInterceptor creatorInterceptor; + public enum ManagerRoleType { None, ResourceOwner, DelegatedResourceOwner } + + public Dictionary Users { get; private set; } public SecurityMatrixTests(ResourceApiFixture fixture, ITestOutputHelper output) @@ -64,9 +70,10 @@ public SecurityMatrixTests(ResourceApiFixture fixture, ITestOutputHelper output) var resourceOwner = fixture.AddProfile(FusionAccountType.Employee); resourceOwner.IsResourceOwner = true; + + var resourceOwnerCreator = fixture.AddProfile(FusionAccountType.Employee); - resourceOwnerCreator.IsResourceOwner = true; - resourceOwnerCreator.FullDepartment = TestDepartment; + SetupManagerRole(ManagerRoleType.ResourceOwner, resourceOwnerCreator, TestDepartment); var taskOwner = fixture.AddProfile(FusionAccountType.Employee); //var taskOwnerBasePosition = testProject.AddBasePosition($"TO: {Guid.NewGuid()}"); @@ -100,18 +107,21 @@ public SecurityMatrixTests(ResourceApiFixture fixture, ITestOutputHelper output) public Task InitializeAsync() => Task.CompletedTask; [Theory] - [InlineData("resourceOwner", TestDepartment, false)] - [InlineData("resourceOwner", SiblingDepartment, false)] - [InlineData("resourceOwner", ParentDepartment, false)] - [InlineData("resourceOwner", SameL2Department, false)] - [InlineData("resourceOwnerRole", ExactScope, false)] - [InlineData("resourceOwnerRole", WildcardScope, false)] - [InlineData("resourceOwnerRole", UnrelatedScope, false)] - public async Task CanDeleteRequestAssignedToDepartment(string role, string department, bool shouldBeAllowed) + [InlineData(ManagerRoleType.ResourceOwner, TestDepartment, false)] + [InlineData(ManagerRoleType.ResourceOwner, SiblingDepartment, false)] + [InlineData(ManagerRoleType.ResourceOwner, ParentDepartment, false)] + [InlineData(ManagerRoleType.ResourceOwner, SameL2Department, false)] + [InlineData(ManagerRoleType.DelegatedResourceOwner, ExactScope, false)] + [InlineData(ManagerRoleType.DelegatedResourceOwner, WildcardScope, false)] + [InlineData(ManagerRoleType.DelegatedResourceOwner, UnrelatedScope, false)] + public async Task CanDeleteRequestAssignedToDepartment(ManagerRoleType role, string department, bool shouldBeAllowed) { var request = await CreateAndStartRequest(); - var user = GetUser(role, department); - using var userScope = fixture.UserScope(user); + + var actor = fixture.AddProfile(FusionAccountType.Employee); + SetupManagerRole(role, actor, department); + + using var userScope = fixture.UserScope(actor); var client = fixture.ApiFactory.CreateClient(); var result = await client.TestClientDeleteAsync($"/projects/{testProject.Project.ProjectId}/requests/{request.Id}"); @@ -121,18 +131,21 @@ public async Task CanDeleteRequestAssignedToDepartment(string role, string depar } [Theory] - [InlineData("resourceOwner", TestDepartment, true)] - [InlineData("resourceOwner", SiblingDepartment, true)] - [InlineData("resourceOwner", ParentDepartment, true)] - [InlineData("resourceOwner", SameL2Department, true)] - [InlineData("resourceOwnerRole", ExactScope, true)] - [InlineData("resourceOwnerRole", WildcardScope, true)] - [InlineData("resourceOwnerRole", UnrelatedScope, false)] - public async Task CanReadRequestsAssignedToDepartment(string role, string department, bool shouldBeAllowed) + [InlineData(ManagerRoleType.ResourceOwner, TestDepartment, true)] + [InlineData(ManagerRoleType.ResourceOwner, SiblingDepartment, true)] + [InlineData(ManagerRoleType.ResourceOwner, ParentDepartment, true)] + [InlineData(ManagerRoleType.ResourceOwner, SameL2Department, true)] + [InlineData(ManagerRoleType.DelegatedResourceOwner, ExactScope, true)] + [InlineData(ManagerRoleType.DelegatedResourceOwner, WildcardScope, true)] + [InlineData(ManagerRoleType.DelegatedResourceOwner, UnrelatedScope, false)] + public async Task CanReadRequestsAssignedToDepartment(ManagerRoleType role, string department, bool shouldBeAllowed) { var request = await CreateAndStartRequest(); - var user = GetUser(role, department); - using var userScope = fixture.UserScope(user); + + var actor = fixture.AddProfile(FusionAccountType.Employee); + SetupManagerRole(role, actor, department); + + using var userScope = fixture.UserScope(actor); var client = fixture.ApiFactory.CreateClient(); var result = await client.TestClientGetAsync($"/departments/{request.AssignedDepartment}/resources/requests/{request.Id}"); @@ -142,18 +155,21 @@ public async Task CanReadRequestsAssignedToDepartment(string role, string depart } [Theory] - [InlineData("resourceOwner", TestDepartment, true)] - [InlineData("resourceOwner", SiblingDepartment, true)] - [InlineData("resourceOwner", ParentDepartment, true)] - [InlineData("resourceOwner", SameL2Department, true)] - [InlineData("resourceOwnerRole", ExactScope, true)] - [InlineData("resourceOwnerRole", WildcardScope, true)] - [InlineData("resourceOwnerRole", UnrelatedScope, false)] - public async Task CanEditGeneralOnRequestAssignedToDepartment(string role, string department, bool shouldBeAllowed) + [InlineData(ManagerRoleType.ResourceOwner, TestDepartment, true)] + [InlineData(ManagerRoleType.ResourceOwner, SiblingDepartment, true)] + [InlineData(ManagerRoleType.ResourceOwner, ParentDepartment, true)] + [InlineData(ManagerRoleType.ResourceOwner, SameL2Department, true)] + [InlineData(ManagerRoleType.DelegatedResourceOwner, ExactScope, true)] + [InlineData(ManagerRoleType.DelegatedResourceOwner, WildcardScope, true)] + [InlineData(ManagerRoleType.DelegatedResourceOwner, UnrelatedScope, false)] + public async Task CanEditGeneralOnRequestAssignedToDepartment(ManagerRoleType role, string department, bool shouldBeAllowed) { var request = await CreateAndStartRequest(); - var user = GetUser(role, department); - using var userScope = fixture.UserScope(user); + + var actor = fixture.AddProfile(FusionAccountType.Employee); + SetupManagerRole(role, actor, department); + + using var userScope = fixture.UserScope(actor); var client = fixture.ApiFactory.CreateClient(); var result = await client.TestClientPatchAsync( @@ -172,18 +188,20 @@ public async Task CanEditGeneralOnRequestAssignedToDepartment(string role, strin } [Theory] - [InlineData("resourceOwner", TestDepartment, false)] - [InlineData("resourceOwner", SiblingDepartment, false)] - [InlineData("resourceOwner", ParentDepartment, false)] - [InlineData("resourceOwner", SameL2Department, false)] - [InlineData("resourceOwnerRole", ExactScope, false)] - [InlineData("resourceOwnerRole", WildcardScope, false)] - [InlineData("resourceOwnerRole", UnrelatedScope, false)] - public async Task CanEditAdditionalCommentOnRequestAssignedToDepartment(string role, string department, bool shouldBeAllowed) + [InlineData(ManagerRoleType.ResourceOwner, TestDepartment, false)] + [InlineData(ManagerRoleType.ResourceOwner, SiblingDepartment, false)] + [InlineData(ManagerRoleType.ResourceOwner, ParentDepartment, false)] + [InlineData(ManagerRoleType.ResourceOwner, SameL2Department, false)] + [InlineData(ManagerRoleType.DelegatedResourceOwner, ExactScope, false)] + [InlineData(ManagerRoleType.DelegatedResourceOwner, WildcardScope, false)] + [InlineData(ManagerRoleType.DelegatedResourceOwner, UnrelatedScope, false)] + public async Task CanEditAdditionalCommentOnRequestAssignedToDepartment(ManagerRoleType role, string department, bool shouldBeAllowed) { var request = await CreateAndStartRequest(); - var user = GetUser(role, department); - using var userScope = fixture.UserScope(user); + var actor = fixture.AddProfile(FusionAccountType.Employee); + SetupManagerRole(role, actor, department); + + using var userScope = fixture.UserScope(actor); var client = fixture.ApiFactory.CreateClient(); var result = await client.TestClientPatchAsync( @@ -199,18 +217,20 @@ public async Task CanEditAdditionalCommentOnRequestAssignedToDepartment(string r } [Theory] - [InlineData("resourceOwner", TestDepartment, true)] - [InlineData("resourceOwner", SiblingDepartment, true)] - [InlineData("resourceOwner", ParentDepartment, true)] - [InlineData("resourceOwner", SameL2Department, true)] - [InlineData("resourceOwnerRole", ExactScope, true)] - [InlineData("resourceOwnerRole", WildcardScope, true)] - [InlineData("resourceOwnerRole", UnrelatedScope, false)] - public async Task CanReassignDepartmentOnRequest(string role, string department, bool shouldBeAllowed) + [InlineData(ManagerRoleType.ResourceOwner, TestDepartment, true)] + [InlineData(ManagerRoleType.ResourceOwner, SiblingDepartment, true)] + [InlineData(ManagerRoleType.ResourceOwner, ParentDepartment, true)] + [InlineData(ManagerRoleType.ResourceOwner, SameL2Department, true)] + [InlineData(ManagerRoleType.DelegatedResourceOwner, ExactScope, true)] + [InlineData(ManagerRoleType.DelegatedResourceOwner, WildcardScope, true)] + [InlineData(ManagerRoleType.DelegatedResourceOwner, UnrelatedScope, false)] + public async Task CanReassignDepartmentOnRequest(ManagerRoleType role, string department, bool shouldBeAllowed) { const string changedDepartment = "TPD UPD ASD"; fixture.EnsureDepartment(changedDepartment); - var user = GetUser(role, department); + var actor = fixture.AddProfile(FusionAccountType.Employee); + SetupManagerRole(role, actor, department); + var request = await CreateAndStartRequest(); using (var adminScope = fixture.AdminScope()) @@ -223,7 +243,7 @@ public async Task CanReassignDepartmentOnRequest(string role, string department, result.Should().BeSuccessfull(); } - using (var userScope = fixture.UserScope(user)) + using (var userScope = fixture.UserScope(actor)) { var client = fixture.ApiFactory.CreateClient(); var result = await client.TestClientPatchAsync( @@ -237,14 +257,14 @@ public async Task CanReassignDepartmentOnRequest(string role, string department, } [Theory] - [InlineData("resourceOwner", TestDepartment, true)] - [InlineData("resourceOwner", SiblingDepartment, true)] - [InlineData("resourceOwner", ParentDepartment, true)] - [InlineData("resourceOwner", SameL2Department, true)] - [InlineData("resourceOwner", "PDP PRD FE ANE ANE5", true)] - [InlineData("resourceOwnerRole", ExactScope, true)] - [InlineData("resourceOwnerRole", WildcardScope, true)] - public async Task CanAssignDepartmentOnUnassignedRequest(string role, string department, bool shouldBeAllowed) + [InlineData(ManagerRoleType.ResourceOwner, TestDepartment, true)] + [InlineData(ManagerRoleType.ResourceOwner, SiblingDepartment, true)] + [InlineData(ManagerRoleType.ResourceOwner, ParentDepartment, true)] + [InlineData(ManagerRoleType.ResourceOwner, SameL2Department, true)] + [InlineData(ManagerRoleType.ResourceOwner, "PDP PRD FE ANE ANE5", true)] + [InlineData(ManagerRoleType.DelegatedResourceOwner, ExactScope, true)] + [InlineData(ManagerRoleType.DelegatedResourceOwner, WildcardScope, true)] + public async Task CanAssignDepartmentOnUnassignedRequest(ManagerRoleType role, string department, bool shouldBeAllowed) { const string changedDepartment = "TDI UPD QWE RTY1"; fixture.EnsureDepartment(changedDepartment); @@ -257,8 +277,10 @@ public async Task CanAssignDepartmentOnUnassignedRequest(string role, string dep .WithTaskOwner(taskOwnerPosition.Id); var request = await CreateAndStartRequest(position); - var user = GetUser(role, department); - using var userScope = fixture.UserScope(user); + var actor = fixture.AddProfile(FusionAccountType.Employee); + SetupManagerRole(role, actor, department); + + using var userScope = fixture.UserScope(actor); var client = fixture.ApiFactory.CreateClient(); var result = await client.TestClientPatchAsync( @@ -271,18 +293,19 @@ public async Task CanAssignDepartmentOnUnassignedRequest(string role, string dep } [Theory] - [InlineData("resourceOwner", TestDepartment, true)] - [InlineData("resourceOwner", SiblingDepartment, true)] - [InlineData("resourceOwner", ParentDepartment, true)] - [InlineData("resourceOwner", SameL2Department, false)] - [InlineData("resourceOwnerRole", ExactScope, true)] - [InlineData("resourceOwnerRole", WildcardScope, true)] - [InlineData("resourceOwnerRole", UnrelatedScope, false)] - public async Task CanCreateResourceOwnerRequest(string role, string department, bool shouldBeAllowed) + [InlineData(ManagerRoleType.ResourceOwner, TestDepartment, true)] + [InlineData(ManagerRoleType.ResourceOwner, SiblingDepartment, true)] + [InlineData(ManagerRoleType.ResourceOwner, ParentDepartment, true)] + [InlineData(ManagerRoleType.ResourceOwner, SameL2Department, false)] + [InlineData(ManagerRoleType.DelegatedResourceOwner, ExactScope, true)] + [InlineData(ManagerRoleType.DelegatedResourceOwner, WildcardScope, true)] + [InlineData(ManagerRoleType.DelegatedResourceOwner, UnrelatedScope, false)] + public async Task CanCreateResourceOwnerRequest(ManagerRoleType role, string department, bool shouldBeAllowed) { - var user = GetUser(role, department); + var actor = fixture.AddProfile(FusionAccountType.Employee); + SetupManagerRole(role, actor, department); - using var userScope = fixture.UserScope(user); + using var userScope = fixture.UserScope(actor); var bp = testProject.AddBasePosition($"{Guid.NewGuid()}", s => s.Department = TestDepartment); var taskOwner = fixture.AddProfile(FusionAccountType.Employee); @@ -317,19 +340,20 @@ public async Task CanCreateResourceOwnerRequest(string role, string department, [Theory] - [InlineData("resourceOwner", TestDepartment, false)] - [InlineData("resourceOwner", SiblingDepartment, false)] - [InlineData("resourceOwner", ParentDepartment, false)] - [InlineData("resourceOwner", SameL2Department, false)] - [InlineData("resourceOwnerRole", ExactScope, false)] - [InlineData("resourceOwnerRole", WildcardScope, false)] - [InlineData("resourceOwnerRole", UnrelatedScope, false)] - public async Task CanStartNormalRequest(string role, string department, bool shouldBeAllowed) + [InlineData(ManagerRoleType.ResourceOwner, TestDepartment, false)] + [InlineData(ManagerRoleType.ResourceOwner, SiblingDepartment, false)] + [InlineData(ManagerRoleType.ResourceOwner, ParentDepartment, false)] + [InlineData(ManagerRoleType.ResourceOwner, SameL2Department, false)] + [InlineData(ManagerRoleType.DelegatedResourceOwner, ExactScope, false)] + [InlineData(ManagerRoleType.DelegatedResourceOwner, WildcardScope, false)] + [InlineData(ManagerRoleType.DelegatedResourceOwner, UnrelatedScope, false)] + public async Task CanStartNormalRequest(ManagerRoleType role, string department, bool shouldBeAllowed) { var request = await CreateRequest(); - var user = GetUser(role, department); + var actor = fixture.AddProfile(FusionAccountType.Employee); + SetupManagerRole(role, actor, department); - using var userScope = fixture.UserScope(user); + using var userScope = fixture.UserScope(actor); var client = fixture.ApiFactory.CreateClient(); var result = await client.TestClientPostAsync( @@ -341,20 +365,21 @@ public async Task CanStartNormalRequest(string role, string department, bool sho } [Theory] - [InlineData("resourceOwner", TestDepartment, true)] - [InlineData("resourceOwner", SiblingDepartment, true)] - [InlineData("resourceOwner", ParentDepartment, true)] - [InlineData("resourceOwner", SameL2Department, true)] - [InlineData("resourceOwnerRole", ExactScope, true)] - [InlineData("resourceOwnerRole", WildcardScope, true)] - [InlineData("resourceOwnerRole", UnrelatedScope, false)] + [InlineData(ManagerRoleType.ResourceOwner, TestDepartment, true)] + [InlineData(ManagerRoleType.ResourceOwner, SiblingDepartment, true)] + [InlineData(ManagerRoleType.ResourceOwner, ParentDepartment, true)] + [InlineData(ManagerRoleType.ResourceOwner, SameL2Department, true)] + [InlineData(ManagerRoleType.DelegatedResourceOwner, ExactScope, true)] + [InlineData(ManagerRoleType.DelegatedResourceOwner, WildcardScope, true)] + [InlineData(ManagerRoleType.DelegatedResourceOwner, UnrelatedScope, false)] //[InlineData("creator", "TPD RND WQE FQE", false)] - public async Task CanProposePersonNormalRequest(string role, string department, bool shouldBeAllowed) + public async Task CanProposePersonNormalRequest(ManagerRoleType role, string department, bool shouldBeAllowed) { var request = await CreateAndStartRequest(); - var user = GetUser(role, department); + var actor = fixture.AddProfile(FusionAccountType.Employee); + SetupManagerRole(role, actor, department); - using var userScope = fixture.UserScope(user); + using var userScope = fixture.UserScope(actor); var proposedPerson = PeopleServiceMock.AddTestProfile() .SaveProfile(); @@ -367,20 +392,26 @@ public async Task CanProposePersonNormalRequest(string role, string department, if (shouldBeAllowed) result.Should().BeSuccessfull(); else result.Should().BeUnauthorized(); } - + public enum ActorType { Manager, TaskOwner, RequestCreator } [Theory] - [InlineData("resourceOwner", TestDepartment, true)] - [InlineData("resourceOwner", SiblingDepartment, true)] - [InlineData("resourceOwner", ParentDepartment, true)] - [InlineData("resourceOwner", SameL2Department, true)] - [InlineData("taskOwner", TestDepartment, false)] - [InlineData("resourceOwnerRole", ExactScope, true)] - [InlineData("resourceOwnerRole", WildcardScope, true)] - [InlineData("resourceOwnerRole", UnrelatedScope, false)] - public async Task CanProposeNormalRequest(string role, string department, bool shouldBeAllowed) + [InlineData(ActorType.Manager, ManagerRoleType.ResourceOwner, TestDepartment, true)] + [InlineData(ActorType.Manager, ManagerRoleType.ResourceOwner, SiblingDepartment, true)] + [InlineData(ActorType.Manager, ManagerRoleType.ResourceOwner, ParentDepartment, true)] + [InlineData(ActorType.Manager, ManagerRoleType.ResourceOwner, SameL2Department, true)] + [InlineData(ActorType.TaskOwner, ManagerRoleType.None, TestDepartment, false)] + [InlineData(ActorType.Manager, ManagerRoleType.DelegatedResourceOwner, ExactScope, true)] + [InlineData(ActorType.Manager, ManagerRoleType.DelegatedResourceOwner, WildcardScope, true)] + [InlineData(ActorType.Manager, ManagerRoleType.DelegatedResourceOwner, UnrelatedScope, false)] + public async Task CanProposeNormalRequest(ActorType actorType, ManagerRoleType role, string department, bool shouldBeAllowed) { var request = await CreateAndStartRequest(); - var user = GetUser(role, department); + var actor = actorType switch + { + ActorType.Manager => fixture.AddProfile(FusionAccountType.Employee), + ActorType.TaskOwner => Users["taskOwner"], + _ => throw new NotSupportedException("Unsupported actor type") + }; + SetupManagerRole(role, actor, department); using (var adminScope = fixture.AdminScope()) { @@ -393,31 +424,34 @@ public async Task CanProposeNormalRequest(string role, string department, bool s await adminClient.ProposePersonAsync(request.Id, proposedPerson); } - using var userScope = fixture.UserScope(user); + using var userScope = fixture.UserScope(actor); var client = fixture.ApiFactory.CreateClient(); - var result = await client.TestClientPostAsync( - $"/departments/{TestDepartment}/resources/requests/{request.Id}/approve", - null - ); + var result = await client.TestClientPostAsync($"/departments/{TestDepartment}/resources/requests/{request.Id}/approve", null); if (shouldBeAllowed) result.Should().BeSuccessfull(); else result.Should().BeUnauthorized(); } [Theory] - [InlineData("resourceOwner", TestDepartment, false)] - [InlineData("resourceOwner", SiblingDepartment, false)] - [InlineData("resourceOwner", ParentDepartment, false)] - [InlineData("resourceOwner", SameL2Department, false)] - [InlineData("taskOwner", TestDepartment, true)] - [InlineData("resourceOwnerRole", ExactScope, false)] - [InlineData("resourceOwnerRole", WildcardScope, false)] - [InlineData("resourceOwnerRole", UnrelatedScope, false)] - public async Task CanAcceptNormalRequest(string role, string department, bool shouldBeAllowed) + [InlineData(ActorType.Manager, ManagerRoleType.ResourceOwner, TestDepartment, false)] + [InlineData(ActorType.Manager, ManagerRoleType.ResourceOwner, SiblingDepartment, false)] + [InlineData(ActorType.Manager, ManagerRoleType.ResourceOwner, ParentDepartment, false)] + [InlineData(ActorType.Manager, ManagerRoleType.ResourceOwner, SameL2Department, false)] + [InlineData(ActorType.TaskOwner, ManagerRoleType.None, TestDepartment, true)] + [InlineData(ActorType.Manager, ManagerRoleType.DelegatedResourceOwner, ExactScope, false)] + [InlineData(ActorType.Manager, ManagerRoleType.DelegatedResourceOwner, WildcardScope, false)] + [InlineData(ActorType.Manager, ManagerRoleType.DelegatedResourceOwner, UnrelatedScope, false)] + public async Task CanAcceptNormalRequest(ActorType actorType, ManagerRoleType role, string department, bool shouldBeAllowed) { var request = await CreateAndStartRequest(); - var user = GetUser(role, department); + var actor = actorType switch + { + ActorType.Manager => fixture.AddProfile(FusionAccountType.Employee), + ActorType.TaskOwner => Users["taskOwner"], + _ => throw new NotSupportedException("Unsupported actor type") + }; + SetupManagerRole(role, actor, department); using (var adminScope = fixture.AdminScope()) { @@ -434,8 +468,8 @@ await adminClient.TestClientPostAsync( OrgRequestInterceptor taskOwnerInterceptor = null; - using var userScope = fixture.UserScope(user); - if (role == "taskOwner") + using var userScope = fixture.UserScope(actor); + if (actorType == ActorType.TaskOwner) { taskOwnerInterceptor = OrgRequestMocker .InterceptOption($"/{testPosition.Id}") @@ -455,16 +489,17 @@ await adminClient.TestClientPostAsync( } [Theory] - [InlineData("resourceOwner", TestDepartment, true)] - [InlineData("resourceOwner", SiblingDepartment, true)] - [InlineData("resourceOwner", ParentDepartment, true)] - [InlineData("resourceOwner", SameL2Department, true)] - [InlineData("resourceOwnerCreator", TestDepartment, true)] - [InlineData("taskOwner", TestDepartment, false)] - [InlineData("resourceOwnerRole", ExactScope, true)] - [InlineData("resourceOwnerRole", WildcardScope, true)] - [InlineData("resourceOwnerRole", UnrelatedScope, false)] - public async Task CanStartChangeRequest(string role, string department, bool shouldBeAllowed) + [InlineData(ActorType.Manager, ManagerRoleType.ResourceOwner, TestDepartment, true)] + [InlineData(ActorType.Manager, ManagerRoleType.ResourceOwner, SiblingDepartment, true)] + [InlineData(ActorType.Manager, ManagerRoleType.ResourceOwner, ParentDepartment, true)] + [InlineData(ActorType.Manager, ManagerRoleType.ResourceOwner, SameL2Department, true)] + // This should be reconsidered. Just being the creator should not give you any additional access, as roles can be changed. + [InlineData(ActorType.RequestCreator, ManagerRoleType.None, TestDepartment, true)] + [InlineData(ActorType.TaskOwner, ManagerRoleType.None, TestDepartment, false)] + [InlineData(ActorType.Manager, ManagerRoleType.DelegatedResourceOwner, ExactScope, true)] + [InlineData(ActorType.Manager, ManagerRoleType.DelegatedResourceOwner, WildcardScope, true)] + [InlineData(ActorType.Manager, ManagerRoleType.DelegatedResourceOwner, UnrelatedScope, false)] + public async Task CanStartChangeRequest(ActorType actorType, ManagerRoleType role, string department, bool shouldBeAllowed) { var request = await CreateChangeRequest(TestDepartment); @@ -476,39 +511,49 @@ public async Task CanStartChangeRequest(string role, string department, bool sho await client.SetChangeParamsAsync(request.Id, DateTime.Today.AddDays(1)); await client.ProposePersonAsync(request.Id, testUser); } - - var user = GetUser(role, department); - using var userScope = fixture.UserScope(user); - var result = await client.TestClientPostAsync( - $"/departments/{request.AssignedDepartment}/resources/requests/{request.Id}/start", - null - ); + var actor = actorType switch + { + ActorType.Manager => fixture.AddProfile(FusionAccountType.Employee), + ActorType.TaskOwner => Users["taskOwner"], + ActorType.RequestCreator=> Users["resourceOwnerCreator"], + _ => throw new NotSupportedException("Unsupported actor type") + }; + SetupManagerRole(role, actor, department); + + using var userScope = fixture.UserScope(actor); + + var result = await client.TestClientPostAsync($"/departments/{request.AssignedDepartment}/resources/requests/{request.Id}/start", null); if (shouldBeAllowed) result.Should().BeSuccessfull(); else result.Should().BeUnauthorized(); } [Theory] - [InlineData("resourceOwner", TestDepartment, true)] - [InlineData("resourceOwner", SiblingDepartment, true)] - [InlineData("resourceOwner", ParentDepartment, true)] - [InlineData("resourceOwner", SameL2Department, false)] - [InlineData("resourceOwnerRole", ExactScope, true)] - [InlineData("resourceOwnerRole", WildcardScope, true)] - [InlineData("resourceOwnerRole", UnrelatedScope, false)] - public async Task CanAddPersonAbsence(string role, string department, bool shouldBeAllowed) + [InlineData(ManagerRoleType.ResourceOwner, TestDepartment, true)] + [InlineData(ManagerRoleType.ResourceOwner, SiblingDepartment, true)] + [InlineData(ManagerRoleType.ResourceOwner, ParentDepartment, true)] + [InlineData(ManagerRoleType.ResourceOwner, SameL2Department, false)] + [InlineData(ManagerRoleType.DelegatedResourceOwner, ExactScope, true)] + [InlineData(ManagerRoleType.DelegatedResourceOwner, WildcardScope, true)] + [InlineData(ManagerRoleType.DelegatedResourceOwner, UnrelatedScope, false)] + public async Task CanAddPersonAbsence(ManagerRoleType role, string department, bool shouldBeAllowed) { - var testUser = fixture.AddProfile(FusionAccountType.Employee); - testUser.FullDepartment = TestDepartment; + var client = fixture.ApiFactory.CreateClient(); - var user = GetUser(role, department); - using var userScope = fixture.UserScope(user); + // Setup the subject we want to create absence on + var testSubject = fixture.AddProfile(FusionAccountType.Employee); + testSubject.FullDepartment = TestDepartment; - var client = fixture.ApiFactory.CreateClient(); + + // Create the actor we want to confirm permissions for + var testUser = fixture.AddProfile(FusionAccountType.Employee); + SetupManagerRole(role, testUser, department); + + using var userScope = fixture.UserScope(testUser); var result = await client.TestClientPostAsync( - $"/persons/{testUser.AzureUniqueId}/absence", + $"/persons/{testSubject.AzureUniqueId}/absence", new CreatePersonAbsenceRequest { AppliesFrom = new DateTime(2021, 04, 30), @@ -524,21 +569,24 @@ public async Task CanAddPersonAbsence(string role, string department, bool shoul } [Theory] - [InlineData("resourceOwner", TestDepartment, true)] - [InlineData("resourceOwner", SiblingDepartment, true)] - [InlineData("resourceOwner", ParentDepartment, true)] - [InlineData("resourceOwner", SameL2Department, false)] - [InlineData("resourceOwnerRole", ExactScope, true)] - [InlineData("resourceOwnerRole", WildcardScope, true)] - [InlineData("resourceOwnerRole", UnrelatedScope, false)] - public async Task CanEditPersonAbsence(string role, string department, bool shouldBeAllowed) + [InlineData(ManagerRoleType.ResourceOwner, TestDepartment, true)] + [InlineData(ManagerRoleType.ResourceOwner, SiblingDepartment, true)] + [InlineData(ManagerRoleType.ResourceOwner, ParentDepartment, true)] + [InlineData(ManagerRoleType.ResourceOwner, SameL2Department, false)] + [InlineData(ManagerRoleType.DelegatedResourceOwner, ExactScope, true)] + [InlineData(ManagerRoleType.DelegatedResourceOwner, WildcardScope, true)] + [InlineData(ManagerRoleType.DelegatedResourceOwner, UnrelatedScope, false)] + public async Task CanEditPersonAbsence(ManagerRoleType role, string department, bool shouldBeAllowed) { + var client = fixture.ApiFactory.CreateClient(); var absence = await CreateAbsence(); - var user = GetUser(role, department); - using var userScope = fixture.UserScope(user); + // Create the actor we want to confirm permissions for + var actor = fixture.AddProfile(FusionAccountType.Employee); + SetupManagerRole(role, actor, department); + + using var userScope = fixture.UserScope(actor); - var client = fixture.ApiFactory.CreateClient(); var result = await client.TestClientPutAsync( $"/persons/{testUser.AzureUniqueId}/absence/{absence.Id}", @@ -557,19 +605,21 @@ public async Task CanEditPersonAbsence(string role, string department, bool shou } [Theory] - [InlineData("resourceOwner", TestDepartment, true)] - [InlineData("resourceOwner", SiblingDepartment, true)] - [InlineData("resourceOwner", ParentDepartment, true)] - [InlineData("resourceOwner", SameL2Department, false)] - [InlineData("resourceOwnerRole", ExactScope, true)] - [InlineData("resourceOwnerRole", WildcardScope, true)] - [InlineData("resourceOwnerRole", UnrelatedScope, false)] - public async Task CanDeletePersonAbsence(string role, string department, bool shouldBeAllowed) + [InlineData(ManagerRoleType.ResourceOwner, TestDepartment, true)] + [InlineData(ManagerRoleType.ResourceOwner, SiblingDepartment, true)] + [InlineData(ManagerRoleType.ResourceOwner, ParentDepartment, true)] + [InlineData(ManagerRoleType.ResourceOwner, SameL2Department, false)] + [InlineData(ManagerRoleType.DelegatedResourceOwner, ExactScope, true)] + [InlineData(ManagerRoleType.DelegatedResourceOwner, WildcardScope, true)] + [InlineData(ManagerRoleType.DelegatedResourceOwner, UnrelatedScope, false)] + public async Task CanDeletePersonAbsence(ManagerRoleType role, string department, bool shouldBeAllowed) { var absence = await CreateAbsence(); - var user = GetUser(role, department); - using var userScope = fixture.UserScope(user); + var actor = fixture.AddProfile(FusionAccountType.Employee); + SetupManagerRole(role, actor, department); + + using var userScope = fixture.UserScope(actor); var client = fixture.ApiFactory.CreateClient(); @@ -582,19 +632,21 @@ public async Task CanDeletePersonAbsence(string role, string department, bool sh } [Theory] - [InlineData("resourceOwner", TestDepartment, true)] - [InlineData("resourceOwner", SiblingDepartment, true)] - [InlineData("resourceOwner", ParentDepartment, true)] - [InlineData("resourceOwner", SameL2Department, true)] - [InlineData("resourceOwnerRole", ExactScope, true)] - [InlineData("resourceOwnerRole", WildcardScope, true)] - [InlineData("resourceOwnerRole", UnrelatedScope, false)] - public async Task CanGetPersonAbsence(string role, string department, bool shouldBeAllowed) + [InlineData(ManagerRoleType.ResourceOwner, TestDepartment, true)] + [InlineData(ManagerRoleType.ResourceOwner, SiblingDepartment, true)] + [InlineData(ManagerRoleType.ResourceOwner, ParentDepartment, true)] + [InlineData(ManagerRoleType.ResourceOwner, SameL2Department, true)] + [InlineData(ManagerRoleType.DelegatedResourceOwner, ExactScope, true)] + [InlineData(ManagerRoleType.DelegatedResourceOwner, WildcardScope, true)] + [InlineData(ManagerRoleType.DelegatedResourceOwner, UnrelatedScope, false)] + public async Task CanGetPersonAbsence(ManagerRoleType role, string department, bool shouldBeAllowed) { var absence = await CreateAbsence(); - var user = GetUser(role, department); - using var userScope = fixture.UserScope(user); + var actor = fixture.AddProfile(FusionAccountType.Employee); + SetupManagerRole(role, actor, department); + + using var userScope = fixture.UserScope(actor); var client = fixture.ApiFactory.CreateClient(); @@ -606,118 +658,142 @@ public async Task CanGetPersonAbsence(string role, string department, bool shoul else result.Should().BeUnauthorized(); } + public enum AbsenceAccessLevel{ None, All, Limited, OtherTasksOnly } [Theory] - [InlineData("resourceOwner", TestDepartment, true)] - [InlineData("resourceOwner", SiblingDepartment, true)] - [InlineData("resourceOwner", ParentDepartment, true)] - [InlineData("resourceOwner", SameL2Department, true)] - [InlineData("resourceOwnerRole", ExactScope, true)] - [InlineData("resourceOwnerRole", WildcardScope, true)] - [InlineData("resourceOwnerRole", UnrelatedScope, true)] // Switch from false to true, as this grants limited access when employee - [InlineData("consultant", UnrelatedScope, false)] - public async Task CanGetAllAbsenceForPerson(string role, string department, bool shouldBeAllowed) + [InlineData(FusionAccountType.Employee, ManagerRoleType.ResourceOwner, TestDepartment, AbsenceAccessLevel.All)] + [InlineData(FusionAccountType.Employee, ManagerRoleType.ResourceOwner, SiblingDepartment, AbsenceAccessLevel.All)] + [InlineData(FusionAccountType.Employee, ManagerRoleType.ResourceOwner, ParentDepartment, AbsenceAccessLevel.All)] + [InlineData(FusionAccountType.Employee, ManagerRoleType.ResourceOwner, SameL2Department, AbsenceAccessLevel.Limited)] + [InlineData(FusionAccountType.Employee, ManagerRoleType.DelegatedResourceOwner, ExactScope, AbsenceAccessLevel.All)] + [InlineData(FusionAccountType.Employee, ManagerRoleType.DelegatedResourceOwner, WildcardScope, AbsenceAccessLevel.All)] + [InlineData(FusionAccountType.Employee, ManagerRoleType.DelegatedResourceOwner, UnrelatedScope, AbsenceAccessLevel.OtherTasksOnly)] + [InlineData(FusionAccountType.Employee, ManagerRoleType.None, UnrelatedScope, AbsenceAccessLevel.OtherTasksOnly)] + [InlineData(FusionAccountType.Consultant, ManagerRoleType.None, UnrelatedScope, AbsenceAccessLevel.None)] + public async Task CanGetAllAbsenceForPerson(FusionAccountType accountType, ManagerRoleType role, string department, AbsenceAccessLevel accessLevel) { var absence = await CreateAbsence(); + var privateAdditionlTask = await CreateAdditionlTask(true); + var additionlTask = await CreateAdditionlTask(false); - var user = role switch { - "consultant" => fixture.AddProfile(FusionAccountType.Consultant), - _ => GetUser(role, department) - }; - using var userScope = fixture.UserScope(user); + + var actor = fixture.AddProfile(accountType); + SetupManagerRole(role, actor, department); + + using var userScope = fixture.UserScope(actor); var client = fixture.ApiFactory.CreateClient(); - var result = await client.TestClientGetAsync( - $"/persons/{testUser.AzureUniqueId}/absence/" - ); + var result = await client.TestClientGetAsync($"/persons/{testUser.AzureUniqueId}/absence"); - if (shouldBeAllowed) result.Should().BeSuccessfull(); - else result.Should().BeUnauthorized(); + + switch (accessLevel) + { + case AbsenceAccessLevel.All: + case AbsenceAccessLevel.Limited: // Limited auth should contain all elements, but hide details if marked private. + result.Should().BeSuccessfull(); + result.Value.Value.Should().Contain(a => a.Id == absence.Id, "should contain absence"); + result.Value.Value.Should().Contain(a => a.Id == additionlTask.Id, "should contain other task"); + result.Value.Value.Should().Contain(a => a.Id == privateAdditionlTask.Id, "should contain private other task"); + break; + + case AbsenceAccessLevel.OtherTasksOnly: + result.Should().BeSuccessfull(); + result.Value.Value.Should().NotContain(a => a.Id == absence.Id, "absence should not be displayed in limited mode"); + result.Value.Value.Should().NotContain(a => a.Id == privateAdditionlTask.Id, "should not contain private other task"); + result.Value.Value.Should().Contain(a => a.Id == additionlTask.Id, "should contain other task"); + break; + + case AbsenceAccessLevel.None: + result.Should().BeUnauthorized(); + break; + + + } + //if (shouldBeAllowed) result.Should().BeSuccessfull(); + //else result.Should().BeUnauthorized(); } [Theory] - [InlineData("resourceOwner", TestDepartment, "GET,POST")] - [InlineData("resourceOwner", SiblingDepartment, "GET,POST")] - [InlineData("resourceOwner", ParentDepartment, "GET,POST")] - [InlineData("resourceOwner", SameL2Department, "GET,!POST")] - [InlineData("resourceOwnerRole", ExactScope, "GET,POST")] - [InlineData("resourceOwnerRole", WildcardScope, "GET,POST")] - [InlineData("resourceOwnerRole", UnrelatedScope, "GET,!POST")] - [InlineData("consultant", UnrelatedScope, "!GEET,!POST")] - public async Task CanGetAbsenceOptionsForPerson(string role, string department, string allowed) + [InlineData(FusionAccountType.Employee, ManagerRoleType.ResourceOwner, TestDepartment, "GET,POST")] + [InlineData(FusionAccountType.Employee, ManagerRoleType.ResourceOwner, SiblingDepartment, "GET,POST")] + [InlineData(FusionAccountType.Employee, ManagerRoleType.ResourceOwner, ParentDepartment, "GET,POST")] + [InlineData(FusionAccountType.Employee, ManagerRoleType.ResourceOwner, SameL2Department, "GET,!POST")] + [InlineData(FusionAccountType.Employee, ManagerRoleType.DelegatedResourceOwner, ExactScope, "GET,POST")] + [InlineData(FusionAccountType.Employee, ManagerRoleType.DelegatedResourceOwner, WildcardScope, "GET,POST")] + [InlineData(FusionAccountType.Employee, ManagerRoleType.DelegatedResourceOwner, UnrelatedScope, "GET,!POST")] + [InlineData(FusionAccountType.Consultant, ManagerRoleType.None, UnrelatedScope, "!GET,!POST")] + public async Task CanGetAbsenceOptionsForPerson(FusionAccountType accountType, ManagerRoleType role, string department, string allowed) { - var user = role switch - { - "consultant" => fixture.AddProfile(FusionAccountType.Consultant), - _ => GetUser(role, department) - }; - using var userScope = fixture.UserScope(user); + var actor = fixture.AddProfile(accountType); + SetupManagerRole(role, actor, department); + + using var userScope = fixture.UserScope(actor); var client = fixture.ApiFactory.CreateClient(); - var result = await client.TestClientOptionsAsync( - $"/persons/{testUser.AzureUniqueId}/absence" - ); + var result = await client.TestClientOptionsAsync($"/persons/{testUser.AzureUniqueId}/absence"); result.CheckAllowHeader(allowed); } [Theory] - [InlineData("resourceOwner", TestDepartment, "GET,PUT,DELETE")] - [InlineData("resourceOwner", SiblingDepartment, "GET,PUT,DELETE")] - [InlineData("resourceOwner", ParentDepartment, "GET,PUT,DELETE")] - [InlineData("resourceOwner", SameL2Department, "GET,!PUT,!DELETE")] - [InlineData("resourceOwnerRole", ExactScope, "GET,PUT,DELETE")] - [InlineData("resourceOwnerRole", WildcardScope, "GET,PUT,DELETE")] - public async Task CanGetAbsenceOptions(string role, string department, string allowed) + [InlineData(ManagerRoleType.ResourceOwner, TestDepartment, "GET,PUT,DELETE")] + [InlineData(ManagerRoleType.ResourceOwner, SiblingDepartment, "GET,PUT,DELETE")] + [InlineData(ManagerRoleType.ResourceOwner, ParentDepartment, "GET,PUT,DELETE")] + [InlineData(ManagerRoleType.ResourceOwner, SameL2Department, "GET,!PUT,!DELETE")] + [InlineData(ManagerRoleType.DelegatedResourceOwner, ExactScope, "GET,PUT,DELETE")] + [InlineData(ManagerRoleType.DelegatedResourceOwner, WildcardScope, "GET,PUT,DELETE")] + public async Task CanGetAbsenceOptions(ManagerRoleType role, string department, string allowed) { var absence = await CreateAbsence(); - var user = GetUser(role, department); - using var userScope = fixture.UserScope(user); + var actor = fixture.AddProfile(FusionAccountType.Employee); + SetupManagerRole(role, actor, department); + + using var userScope = fixture.UserScope(actor); var client = fixture.ApiFactory.CreateClient(); - var result = await client.TestClientOptionsAsync( - $"/persons/{testUser.AzureUniqueId}/absence/{absence.Id}" - ); + var result = await client.TestClientOptionsAsync($"/persons/{testUser.AzureUniqueId}/absence/{absence.Id}"); result.CheckAllowHeader(allowed); } [Theory] - [InlineData("resourceOwner", TestDepartment, true)] - [InlineData("resourceOwner", SiblingDepartment, true)] - [InlineData("resourceOwner", ParentDepartment, true)] - [InlineData("resourceOwner", SameL2Department, true)] - [InlineData("resourceOwnerRole", ExactScope, true)] - [InlineData("resourceOwnerRole", WildcardScope, true)] - public async Task CanGetDepartmentUnassignedRequests(string role, string department, bool shouldBeAllowed) + [InlineData(ManagerRoleType.ResourceOwner, TestDepartment, true)] + [InlineData(ManagerRoleType.ResourceOwner, SiblingDepartment, true)] + [InlineData(ManagerRoleType.ResourceOwner, ParentDepartment, true)] + [InlineData(ManagerRoleType.ResourceOwner, SameL2Department, true)] + [InlineData(ManagerRoleType.DelegatedResourceOwner, ExactScope, true)] + [InlineData(ManagerRoleType.DelegatedResourceOwner, WildcardScope, true)] + public async Task CanGetDepartmentUnassignedRequests(ManagerRoleType role, string department, bool shouldBeAllowed) { fixture.EnsureDepartment(TestDepartment); - var user = GetUser(role, department); - using var userScope = fixture.UserScope(user); + + var actor = fixture.AddProfile(FusionAccountType.Employee); + SetupManagerRole(role, actor, department); + + using var userScope = fixture.UserScope(actor); var client = fixture.ApiFactory.CreateClient(); - var result = await client.TestClientGetAsync( - $"/departments/{TestDepartment}/resources/requests/unassigned" - ); + var result = await client.TestClientGetAsync($"/departments/{TestDepartment}/resources/requests/unassigned"); if (shouldBeAllowed) result.Should().BeSuccessfull(); else result.Should().BeUnauthorized(); } [Theory] - [InlineData("resourceOwner", TestDepartment, "GET,PATCH")] - [InlineData("resourceOwner", SiblingDepartment, "GET,PATCH")] - [InlineData("resourceOwner", ParentDepartment, "GET,PATCH")] - [InlineData("resourceOwner", SameL2Department, "GET,PATCH")] - [InlineData("resourceOwnerRole", ExactScope, "GET,PATCH")] - [InlineData("resourceOwnerRole", WildcardScope, "GET,PATCH")] - public async Task CanGetOptionsDepartmentUnassignedRequests(string role, string department, string allowedVerbs) + [InlineData(ManagerRoleType.ResourceOwner, TestDepartment, "GET,PATCH")] + [InlineData(ManagerRoleType.ResourceOwner, SiblingDepartment, "GET,PATCH")] + [InlineData(ManagerRoleType.ResourceOwner, ParentDepartment, "GET,PATCH")] + [InlineData(ManagerRoleType.ResourceOwner, SameL2Department, "GET,PATCH")] + [InlineData(ManagerRoleType.DelegatedResourceOwner, ExactScope, "GET,PATCH")] + [InlineData(ManagerRoleType.DelegatedResourceOwner, WildcardScope, "GET,PATCH")] + public async Task CanGetOptionsDepartmentUnassignedRequests(ManagerRoleType role, string department, string allowedVerbs) { fixture.EnsureDepartment(TestDepartment); + var request = await CreateChangeRequest(TestDepartment); using (var adminscope = fixture.AdminScope()) @@ -728,8 +804,10 @@ public async Task CanGetOptionsDepartmentUnassignedRequests(string role, string await client.AssignDepartmentAsync(request.Id, null); } - var user = GetUser(role, department); - using (var userScope = fixture.UserScope(user)) + var actor = fixture.AddProfile(FusionAccountType.Employee); + SetupManagerRole(role, actor, department); + + using (var userScope = fixture.UserScope(actor)) { var client = fixture.ApiFactory.CreateClient(); @@ -741,23 +819,25 @@ public async Task CanGetOptionsDepartmentUnassignedRequests(string role, string } [Theory] - [InlineData("resourceOwner", TestDepartment, true)] - [InlineData("resourceOwner", SiblingDepartment, true)] - [InlineData("resourceOwner", ParentDepartment, true)] - [InlineData("resourceOwner", SameL2Department, true)] - [InlineData("resourceOwner", "PDP PRS XXX YYY", false)] - [InlineData("resourceOwner", "CFO GBS XXX YYY", false)] - [InlineData("resourceOwner", "TDI XXX YYY", false)] - [InlineData("resourceOwner", "CFO SBG YYY", false)] - [InlineData("resourceOwnerRole", ExactScope, true)] - [InlineData("resourceOwnerRole", WildcardScope, true)] - [InlineData("resourceOwnerRole", UnrelatedScope, false)] - public async Task CanGetInternalRequests(string role, string department, bool shouldBeAllowed) + [InlineData(ManagerRoleType.ResourceOwner, TestDepartment, true)] + [InlineData(ManagerRoleType.ResourceOwner, SiblingDepartment, true)] + [InlineData(ManagerRoleType.ResourceOwner, ParentDepartment, true)] + [InlineData(ManagerRoleType.ResourceOwner, SameL2Department, true)] + [InlineData(ManagerRoleType.ResourceOwner, "PDP PRS XXX YYY", false)] + [InlineData(ManagerRoleType.ResourceOwner, "CFO GBS XXX YYY", false)] + [InlineData(ManagerRoleType.ResourceOwner, "TDI XXX YYY", false)] + [InlineData(ManagerRoleType.ResourceOwner, "CFO SBG YYY", false)] + [InlineData(ManagerRoleType.DelegatedResourceOwner, ExactScope, true)] + [InlineData(ManagerRoleType.DelegatedResourceOwner, WildcardScope, true)] + [InlineData(ManagerRoleType.DelegatedResourceOwner, UnrelatedScope, false)] + public async Task CanGetInternalRequests(ManagerRoleType role, string department, bool shouldBeAllowed) { fixture.EnsureDepartment(TestDepartment); - var user = GetUser(role, department); - using var userScope = fixture.UserScope(user); + var actor = fixture.AddProfile(FusionAccountType.Employee); + SetupManagerRole(role, actor, department); + + using var userScope = fixture.UserScope(actor); var client = fixture.ApiFactory.CreateClient(); var result = await client.TestClientGetAsync($"/resources/requests/internal?$filter=assignedDepartment eq {TestDepartment}"); @@ -767,23 +847,47 @@ public async Task CanGetInternalRequests(string role, string department, bool sh } + /// + /// Create an absence which is active, today +- 10 days. + /// + /// private async Task CreateAbsence() { using var adminScope = fixture.AdminScope(); var client = fixture.ApiFactory.CreateClient(); - var result = await client.TestClientPostAsync( - $"/persons/{testUser.AzureUniqueId}/absence", - new CreatePersonAbsenceRequest - { - AppliesFrom = new DateTime(2021, 04, 30), - AppliesTo = new DateTime(2022, 04, 30), - Comment = "A comment", - Type = ApiPersonAbsence.ApiAbsenceType.Absence, - AbsencePercentage = 100 - } - ); + var result = await client.TestClientPostAsync($"/persons/{testUser.AzureUniqueId}/absence", new + { + appliesFrom = DateTime.Today.AddDays(-10), + appliesTo = DateTime.Today.AddDays(10), + comment = "A comment", + type = "absence", + absencePercentage = 100 + }); + + return result.Value; + } + /// + /// Create absence of type otherTasks, which is active, today +- 10 days. + /// + /// Should be marked private + /// + private async Task CreateAdditionlTask(bool isPrivate) + { + using var adminScope = fixture.AdminScope(); + + var client = fixture.ApiFactory.CreateClient(); + + var result = await client.TestClientPostAsync($"/persons/{testUser.AzureUniqueId}/absence", new + { + appliesFrom = DateTime.Today.AddDays(-10), + appliesTo = DateTime.Today.AddDays(10), + comment = "A comment", + type = "otherTasks", + absencePercentage = 100, + isPrivate = isPrivate + }); return result.Value; } @@ -812,41 +916,27 @@ private async Task CreateChangeRequest(string depar return req; } - private ApiPersonProfileV3 GetUser(string role, string departmentOrScope) + /// + /// Will set up the provided test user as manager as configured in "SAP", or with a local delegated resource owner. + /// + private void SetupManagerRole(ManagerRoleType role, ApiPersonProfileV3 testUser, string fullDepartment) { - if (role == "resourceOwnerRole") + switch (role) { - return CreateTestUserWithRole(departmentOrScope); - } + case ManagerRoleType.ResourceOwner: + fixture.SetAsResourceOwner(testUser, fullDepartment); + break; + case ManagerRoleType.DelegatedResourceOwner: + testUser.WithDelegatedManagerRole(fullDepartment); + break; + case ManagerRoleType.None: + break; + default: + throw new NotSupportedException("Role setup not supported"); - Users[role].FullDepartment = departmentOrScope; - return Users[role]; + } } - private ApiPersonProfileV3 CreateTestUserWithRole(string scope) - { - var testUser = fixture.AddProfile(FusionAccountType.Employee); - RolesClientMock.AddPersonRole(testUser.AzureUniqueId.Value, new Fusion.Integration.Roles.RoleAssignment - { - Identifier = $"{Guid.NewGuid()}", - RoleName = AccessRoles.ResourceOwner, - Scope = new Fusion.Integration.Roles.RoleAssignment.RoleScope("OrgUnit", scope), - ValidTo = DateTime.UtcNow.AddDays(1), - Source = "Test project" - }); - testUser.Department = "EPN SUB WS WPN"; - testUser.Roles = new List - { - new ApiPersonRoleV3 - { - Name = AccessRoles.ResourceOwner, - Scope = new ApiPersonRoleScopeV3 { Type = "OrgUnit", Value = scope }, - ActiveToUtc = DateTime.UtcNow.AddDays(1), - IsActive = true, - } - }; - return testUser; - } private Task CreateAndStartRequest() => CreateAndStartRequest(testPosition); diff --git a/src/backend/tests/Fusion.Resources.Api.Tests/Data/InternalRequestData.cs b/src/backend/tests/Fusion.Resources.Api.Tests/Data/InternalRequestData.cs index 7c1c49c119..2f275d2d64 100644 --- a/src/backend/tests/Fusion.Resources.Api.Tests/Data/InternalRequestData.cs +++ b/src/backend/tests/Fusion.Resources.Api.Tests/Data/InternalRequestData.cs @@ -29,8 +29,13 @@ static InternalRequestData() foreach (var department in SupportedDepartments) { - var user = PeopleServiceMock.AddTestProfile().SaveProfile(); - LineOrgServiceMock.AddTestUser().MergeWithProfile(user).AsResourceOwner().WithFullDepartment(department).SaveProfile(); + var user = PeopleServiceMock.AddTestProfile() + .WithFullDepartment(department) + .AsResourceOwner() + .SaveProfile(); + + LineOrgServiceMock.AddOrgUnit(department); + LineOrgServiceMock.AddOrgUnitManager(department, user); } } } diff --git a/src/backend/tests/Fusion.Resources.Api.Tests/Fixture/ResourceApiFixture.cs b/src/backend/tests/Fusion.Resources.Api.Tests/Fixture/ResourceApiFixture.cs index 387e409600..b359789d03 100644 --- a/src/backend/tests/Fusion.Resources.Api.Tests/Fixture/ResourceApiFixture.cs +++ b/src/backend/tests/Fusion.Resources.Api.Tests/Fixture/ResourceApiFixture.cs @@ -74,18 +74,60 @@ internal ApiPersonProfileV3 AddProfile(Action + /// Sets the profile as manager for the org unit. + /// Returns the org unit created for the full department string. + /// + /// + /// + /// + internal ApiOrgUnit SetAsResourceOwner(ApiPersonProfileV3 person, string department) + { + person.IsResourceOwner = true; + + DepartmentPath d = new DepartmentPath(department); + + person.FullDepartment = d.Parent(); + person.Department = d.ParentDeparment.GetShortName(); + + // Must add roles.. Create SAP id + var orgUnit = LineOrgServiceMock.AddOrgUnit(department); + + if (person.Roles is null) + person.Roles = new List(); + + person.Roles = new List + { + new ApiPersonRoleV3 + { + Name = "Fusion.LineOrg.Manager", + Scope = new ApiPersonRoleScopeV3 { Type = "OrgUnit", Value = orgUnit.SapId }, + IsActive = true, + OnDemandSupport = false + } + }; + + RolesClientMock.AddPersonRole(person.AzureUniqueId.Value, new Fusion.Integration.Roles.RoleAssignment + { + Identifier = $"{Guid.NewGuid()}", + RoleName = "Fusion.LineOrg.Manager", + Scope = new Fusion.Integration.Roles.RoleAssignment.RoleScope("OrgUnit", orgUnit.SapId), + ValidTo = DateTime.UtcNow.AddDays(1), + Source = "Test project" + }); + + LineOrgServiceMock.AddOrgUnit(department); + LineOrgServiceMock.AddOrgUnitManager(department, person); + + return orgUnit; + } + internal ApiOrgUnit AddOrgUnit(string sapId, string name, string fullDepartment) { return LineOrgServiceMock.AddOrgUnit(sapId, name, fullDepartment, fullDepartment, fullDepartment); diff --git a/src/backend/tests/Fusion.Resources.Api.Tests/IntegrationTests/DepartmentRequestsTests.cs b/src/backend/tests/Fusion.Resources.Api.Tests/IntegrationTests/DepartmentRequestsTests.cs index e9ab8a5f30..a6b9669662 100644 --- a/src/backend/tests/Fusion.Resources.Api.Tests/IntegrationTests/DepartmentRequestsTests.cs +++ b/src/backend/tests/Fusion.Resources.Api.Tests/IntegrationTests/DepartmentRequestsTests.cs @@ -5,14 +5,12 @@ using Fusion.Testing; using Fusion.Testing.Authentication.User; using Fusion.Testing.Mocks; -using Fusion.Testing.Mocks.LineOrgService; using Fusion.Testing.Mocks.OrgService; using System; using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Threading.Tasks; -using Fusion.Testing.Mocks.ProfileService; using Xunit; using Xunit.Abstractions; using Fusion.Services.LineOrg.ApiModels; @@ -44,14 +42,7 @@ public DepartmentRequestsTests(ResourceApiFixture fixture, ITestOutputHelper out public async Task InitializeAsync() { - user = fixture.AddProfile(FusionAccountType.Employee); - user.Department = TimelineDepartment; - user.FullDepartment = TimelineDepartment; - - LineOrgServiceMock.AddTestUser() - .MergeWithProfile(user) - .AsResourceOwner() - .SaveProfile(); + user = fixture.AddResourceOwner(TimelineDepartment); userOrgUnit = fixture.AddOrgUnit(user.FullDepartment); @@ -569,16 +560,7 @@ public async Task GetTimeline_ShouldNotIncludeTaskDetailsForOtherResourceOwner() absence = absenceResp.Value; } - var resourceOwner = fixture.AddProfile(FusionAccountType.Employee); - resourceOwner.IsResourceOwner = true; - resourceOwner.FullDepartment = "TPD TST QWE"; - resourceOwner.Department = "TPD TST QWE"; - - LineOrgServiceMock.AddTestUser() - .MergeWithProfile(resourceOwner) - .AsResourceOwner() - .WithDepartment("TPD TST QWE") - .SaveProfile(); + var resourceOwner = fixture.AddResourceOwner("TPD TST QWE"); using (var userScope = fixture.UserScope(resourceOwner)) { @@ -586,7 +568,7 @@ public async Task GetTimeline_ShouldNotIncludeTaskDetailsForOtherResourceOwner() var timelineEnd = new DateTime(2020, 03, 31); var response = await Client.TestClientGetAsync( - $"/departments/{user.Department}/resources/personnel/?$expand=timeline&{ApiVersion}&timelineStart={timelineStart:O}&timelineEnd={timelineEnd:O}" + $"/departments/{TimelineDepartment}/resources/personnel/?$expand=timeline&{ApiVersion}&timelineStart={timelineStart:O}&timelineEnd={timelineEnd:O}" ); var person = response.Value.value @@ -609,15 +591,16 @@ public async Task GetTimeline_ShouldNotIncludeTaskDetailsForOtherResourceOwner() public async Task GetTimelineShouldNotIncludePositionsOutsideTimeframe() { const string department = "PDP BTAD AWQ"; - fixture.EnsureDepartment(department); + fixture.AddOrgUnit(department); var project = new FusionTestProjectBuilder() .WithPositions(10, 50) .AddToMockService(); - var profile = PeopleServiceMock.AddTestProfile().WithFullDepartment(department); - foreach (var position in project.Positions) profile.WithPosition(position); - profile.SaveProfile(); + var profile = fixture.AddProfile(s => { + s.WithFullDepartment(department); + s.WithPositions(project.Positions); + }); using var scope = fixture.AdminScope(); var timelineStart = new DateTime(2020, 03, 01); @@ -717,15 +700,13 @@ public async Task GetTimeline_ShouldNotIncludeRequestsOutsideTimeframe() public async Task GetRequests_ShouldNotIncludeRequestsOutsideCurrentAllocations() { const string department = "PDP BTAD AWQ"; - fixture.EnsureDepartment(department); + fixture.AddOrgUnit(department); var project = new FusionTestProjectBuilder() .WithPositions(10, 50) .AddToMockService(); - var profile = PeopleServiceMock.AddTestProfile().WithFullDepartment(department); - foreach (var position in project.Positions) profile.WithPosition(position); - profile.SaveProfile(); + var profile = fixture.AddProfile(s => s.WithFullDepartment(department).WithPositions(project.Positions)); using var scope = fixture.AdminScope(); diff --git a/src/backend/tests/Fusion.Resources.Api.Tests/IntegrationTests/DepartmentsControllerTests.cs b/src/backend/tests/Fusion.Resources.Api.Tests/IntegrationTests/DepartmentsControllerTests.cs index 0e0df97ed9..67b810968e 100644 --- a/src/backend/tests/Fusion.Resources.Api.Tests/IntegrationTests/DepartmentsControllerTests.cs +++ b/src/backend/tests/Fusion.Resources.Api.Tests/IntegrationTests/DepartmentsControllerTests.cs @@ -239,14 +239,15 @@ public async Task GetDepartments_Should_GetDelegatedResponsibles_FromGetDepartme public async Task SearchShouldBeCaseInsensitive() { var fakeResourceOwner = fixture.AddProfile(FusionAccountType.Employee); - LineOrgServiceMock.AddTestUser().MergeWithProfile(fakeResourceOwner).AsResourceOwner().SaveProfile(); + var orgUnit = fixture.SetAsResourceOwner(fakeResourceOwner, fakeResourceOwner.FullDepartment); + using var adminScope = fixture.AdminScope(); var resp = await Client.TestClientGetAsync>($"/departments?$search={fakeResourceOwner.Name.ToUpper()}"); resp.Response.StatusCode.Should().Be(HttpStatusCode.OK); - resp.Value.Should().Contain(x => x.Name == fakeResourceOwner.FullDepartment); + resp.Value.Should().Contain(x => x.Name == orgUnit.Name); } [Fact] @@ -285,7 +286,7 @@ public async Task OptionsDepartmentResponsible_CanDelegateAccessToCurrentAndDown fixture.EnsureDepartment(dep); var testDepartment = "AAA BBB CCC XXX"; - var resourceOwner = fixture.AddProfile(x => x.WithAccountType(FusionAccountType.Employee).AsResourceOwner().WithFullDepartment(testDepartment)); + var resourceOwner = fixture.AddResourceOwner(testDepartment); // AddProfile(x => x.WithAccountType(FusionAccountType.Employee).AsResourceOwner().WithFullDepartment(testDepartment)); using var adminScope = fixture.UserScope(resourceOwner); var result = await Client.TestClientOptionsAsync($"/departments/{fullDepartment}/delegated-resource-owners"); @@ -306,7 +307,7 @@ public async Task PostDepartmentResponsible_CanDelegateAccessToCurrentAndDownwar var testDepartment = "AAA BBB CCC XXX"; var delegatedResourceOwner = fixture.AddProfile(FusionAccountType.Employee); - var resourceOwner = fixture.AddProfile(x => x.WithAccountType(FusionAccountType.Employee).AsResourceOwner().WithFullDepartment(testDepartment)); + var resourceOwner = fixture.AddResourceOwner(testDepartment); using var adminScope = fixture.UserScope(resourceOwner); var result = await Client.TestClientPostAsync($"/departments/{fullDepartment}/delegated-resource-owners", new diff --git a/src/backend/tests/Fusion.Resources.Api.Tests/IntegrationTests/InternalRequests/ProjectRequestAccessTests.cs b/src/backend/tests/Fusion.Resources.Api.Tests/IntegrationTests/InternalRequests/ProjectRequestAccessTests.cs index 483e923967..f29ed77ee0 100644 --- a/src/backend/tests/Fusion.Resources.Api.Tests/IntegrationTests/InternalRequests/ProjectRequestAccessTests.cs +++ b/src/backend/tests/Fusion.Resources.Api.Tests/IntegrationTests/InternalRequests/ProjectRequestAccessTests.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; @@ -7,6 +8,8 @@ using Fusion.Integration.Profile; using Fusion.Integration.Profile.ApiClient; using Fusion.Resources.Api.Tests.Fixture; +using Fusion.Resources.Api.Tests.FusionMocks; +using Fusion.Resources.Domain; using Fusion.Testing; using Fusion.Testing.Authentication.User; using Fusion.Testing.Mocks; @@ -156,6 +159,35 @@ public async Task StartAllocationRequest_ShouldNotAccess_WhenProjectMember() public static class ApiPersonProfileV3Extensions { + public static ApiPersonProfileV3 WithDelegatedManagerRole(this ApiPersonProfileV3 profile, string fullDepartment) + { + RolesClientMock.AddPersonRole(profile.AzureUniqueId.Value, new Fusion.Integration.Roles.RoleAssignment + { + Identifier = $"{Guid.NewGuid()}", + RoleName = AccessRoles.ResourceOwner, + Scope = new Fusion.Integration.Roles.RoleAssignment.RoleScope("OrgUnit", fullDepartment), + ValidTo = DateTime.UtcNow.AddDays(1), + Source = "Test project" + }); + + if (profile.Roles is null) + profile.Roles = new List(); + + profile.Roles.Add(new ApiPersonRoleV3 + { + Name = AccessRoles.ResourceOwner, + Scope = new ApiPersonRoleScopeV3 + { + Type = "OrgUnit", + Value = fullDepartment + }, + ActiveToUtc = DateTime.UtcNow.AddDays(1), + IsActive = true, + }); + + return profile; + } + public static ApiPersonProfileV3 WithPosition(this ApiPersonProfileV3 profile, ApiPositionV2 position) { var personPositions = position.Instances diff --git a/src/backend/tests/Fusion.Resources.Api.Tests/IntegrationTests/PersonAbsenceTests.cs b/src/backend/tests/Fusion.Resources.Api.Tests/IntegrationTests/PersonAbsenceTests.cs index e933692fd0..2e28ab4e10 100644 --- a/src/backend/tests/Fusion.Resources.Api.Tests/IntegrationTests/PersonAbsenceTests.cs +++ b/src/backend/tests/Fusion.Resources.Api.Tests/IntegrationTests/PersonAbsenceTests.cs @@ -12,6 +12,7 @@ using Xunit; using Xunit.Abstractions; using Fusion.Testing.Mocks.OrgService; +using System.Collections.Generic; #nullable enable namespace Fusion.Resources.Api.Tests.IntegrationTests { @@ -318,10 +319,7 @@ public async Task CreateAbsence_ShouldUseBasePositionName_WhenRoleNameNull() [Fact] public async Task GetAbsence_ShouldBeHiddenForOtherResourceOwners_WhenPrivate() { - var siblingResourceOwner = fixture.AddProfile(FusionAccountType.Employee); - siblingResourceOwner.FullDepartment = "TPD PRD TST QWE ABC"; - siblingResourceOwner.Department = "TST QWE ABC"; - siblingResourceOwner.IsResourceOwner = true; + var siblingResourceOwner = fixture.AddResourceOwner("TPD PRD TST QWE ABC"); var request = new CreatePersonAbsenceRequest { @@ -503,6 +501,11 @@ public class TestAbsence public TestTaskDetails? TaskDetails { get; set; } } + public class TestAbsenceCollection + { + public List Value { get; set; } + } + public class TestTaskDetails { public bool IsHidden { get; set; } diff --git a/src/backend/tests/Fusion.Resources.Api.Tests/IntegrationTests/PersonNotesTests.cs b/src/backend/tests/Fusion.Resources.Api.Tests/IntegrationTests/PersonNotesTests.cs index f4e1b25a33..07171671b1 100644 --- a/src/backend/tests/Fusion.Resources.Api.Tests/IntegrationTests/PersonNotesTests.cs +++ b/src/backend/tests/Fusion.Resources.Api.Tests/IntegrationTests/PersonNotesTests.cs @@ -41,9 +41,7 @@ public PersonNotesTests(ResourceApiFixture fixture, ITestOutputHelper output) testUser.FullDepartment = "L1 L2 L3 L4"; testUser.Department = "L2 L3 L4"; - resourceOwner = fixture.AddProfile(FusionAccountType.Employee); - resourceOwner.FullDepartment = testUser.FullDepartment; - resourceOwner.IsResourceOwner = true; + resourceOwner = fixture.AddResourceOwner(testUser.FullDepartment); } [Fact] @@ -80,9 +78,9 @@ public async Task Create_ShouldBeSuccessful_WhenResourceOwner() [Fact] public async Task Create_ShouldBeUnauthorized_WhenResourceOwnerInOtherDepartment() { - resourceOwner.FullDepartment = "L1 L2 L3 L4A"; + var otherDepartmentResourceOwner = fixture.AddResourceOwner("L1 L2 L3 L4A"); - using var resOwnerScope = fixture.UserScope(resourceOwner); + using var resOwnerScope = fixture.UserScope(otherDepartmentResourceOwner); var resp = await client.TestClientPostAsync($"persons/{testUser.AzureUniqueId}/resources/notes", new { title = $"Test {Guid.NewGuid()}", @@ -112,8 +110,9 @@ public async Task Delete_ShouldBeSuccessfull_WhenResourceOwner() [Fact] public async Task Delete_ShouldBeUnauthorizer_WhenResourceOwnerInOtherDepartment() { - resourceOwner.FullDepartment = "L1 L2 L3 L4A"; - using var resOwnerScope = fixture.UserScope(resourceOwner); + var otherDepartmentResourceOwner = fixture.AddResourceOwner("L1 L2 L3 L4A"); + + using var resOwnerScope = fixture.UserScope(otherDepartmentResourceOwner); var resp = await client.TestClientDeleteAsync($"persons/{testUser.AzureUniqueId}/resources/notes/{testNoteId}"); resp.Should().BeUnauthorized(); @@ -140,9 +139,10 @@ public async Task Update_ShouldBeSuccessfull_WhenResourceOwner() [InlineData("L1 L2 L3A L4")] public async Task Update_ShouldBeUnauthorized_WhenOtherResourceOwnerIn(string department) { - resourceOwner.FullDepartment = department; - using var resOwnerScope = fixture.UserScope(resourceOwner); + var otherDepartmentResourceOwner = fixture.AddResourceOwner(department); + + using var resOwnerScope = fixture.UserScope(otherDepartmentResourceOwner); var resp = await client.TestClientPutAsync($"persons/{testUser.AzureUniqueId}/resources/notes/{testNoteId}", new { title = $"Updated title {Guid.NewGuid()}", @@ -191,9 +191,9 @@ public async Task GetNotes_ShouldOnlyDisplaySharedNotes_WhenSiblingResourceOwner var privateNote = await CreateNoteAsAdminAsync(client, isShared: true); // Update the department for the resource owner - resourceOwner.FullDepartment = departmentPath; + var otherDepartmentResourceOwner = fixture.AddResourceOwner(departmentPath); - using var resOwnerScope = fixture.UserScope(resourceOwner); + using var resOwnerScope = fixture.UserScope(otherDepartmentResourceOwner); var resp = await client.TestClientGetAsync($"persons/{testUser.AzureUniqueId}/resources/notes", new[] { new { id = Guid.Empty } }); diff --git a/src/backend/tests/Fusion.Resources.Api.Tests/IntegrationTests/RequestActionTests.cs b/src/backend/tests/Fusion.Resources.Api.Tests/IntegrationTests/RequestActionTests.cs index 88d7178174..7ca18dcd96 100644 --- a/src/backend/tests/Fusion.Resources.Api.Tests/IntegrationTests/RequestActionTests.cs +++ b/src/backend/tests/Fusion.Resources.Api.Tests/IntegrationTests/RequestActionTests.cs @@ -60,9 +60,7 @@ public async Task InitializeAsync() normalRequest = await adminClient.CreateDefaultRequestAsync(testProject); // Generate random test user - resourceOwner = fixture.AddProfile(FusionAccountType.Employee); - resourceOwner.IsResourceOwner = true; - resourceOwner.FullDepartment = normalRequest.AssignedDepartment ?? "PDP TST DPT"; + resourceOwner = fixture.AddResourceOwner(normalRequest.AssignedDepartment ?? "PDP TST DPT"); taskOwner = fixture.AddProfile(FusionAccountType.Employee); taskOwnerPosition = testProject.AddPosition() diff --git a/src/backend/tests/Fusion.Resources.Api.Tests/IntegrationTests/RequestComments.cs b/src/backend/tests/Fusion.Resources.Api.Tests/IntegrationTests/RequestComments.cs index 318bd6bfed..348ac6e2c1 100644 --- a/src/backend/tests/Fusion.Resources.Api.Tests/IntegrationTests/RequestComments.cs +++ b/src/backend/tests/Fusion.Resources.Api.Tests/IntegrationTests/RequestComments.cs @@ -45,6 +45,7 @@ public RequestComments(ResourceApiFixture fixture, ITestOutputHelper output) testUser.FullDepartment = testDepartment; testUser.Department = "L2 L3 L4"; + fixture.EnsureDepartment(testDepartment); resourceOwner = fixture.AddResourceOwner(testDepartment); taskOwner = fixture.AddProfile(FusionAccountType.Employee); @@ -80,7 +81,7 @@ public async Task InitializeAsync() pos => pos.WithParentPosition(taskOwnerPosition.Id)); //await adminClient.StartProjectRequestAsync(testProject, request.Id); await adminClient.StartProjectRequestAsync(testProject, request.Id); - await adminClient.AssignDepartmentAsync(request.Id, resourceOwner.FullDepartment); + await adminClient.AssignDepartmentAsync(request.Id, testDepartment); var comment = new { content = "Resource owner gossip." }; var response = await adminClient.TestClientPostAsync($"/resources/requests/internal/{request.Id}/comments", comment); diff --git a/src/backend/tests/Fusion.Resources.Api.Tests/IntegrationTests/RequestConversationTests.cs b/src/backend/tests/Fusion.Resources.Api.Tests/IntegrationTests/RequestConversationTests.cs index 6f59993e92..360f8419a4 100644 --- a/src/backend/tests/Fusion.Resources.Api.Tests/IntegrationTests/RequestConversationTests.cs +++ b/src/backend/tests/Fusion.Resources.Api.Tests/IntegrationTests/RequestConversationTests.cs @@ -60,9 +60,7 @@ public async Task InitializeAsync() normalRequest = await adminClient.CreateDefaultRequestAsync(testProject); // Generate random test user - resourceOwner = fixture.AddProfile(FusionAccountType.Employee); - resourceOwner.IsResourceOwner = true; - resourceOwner.FullDepartment = normalRequest.AssignedDepartment ?? "PDP TST DPT"; + resourceOwner = fixture.AddResourceOwner(normalRequest.AssignedDepartment ?? "PDP TST DPT"); taskOwner = fixture.AddProfile(FusionAccountType.Employee); taskOwner.IsResourceOwner = false; diff --git a/src/backend/tests/Fusion.Resources.Api.Tests/IntegrationTests/ResponsibilityMatrixTests.cs b/src/backend/tests/Fusion.Resources.Api.Tests/IntegrationTests/ResponsibilityMatrixTests.cs index ab282934de..d4d0072496 100644 --- a/src/backend/tests/Fusion.Resources.Api.Tests/IntegrationTests/ResponsibilityMatrixTests.cs +++ b/src/backend/tests/Fusion.Resources.Api.Tests/IntegrationTests/ResponsibilityMatrixTests.cs @@ -133,7 +133,7 @@ public async Task PutMatrix_ShouldBeBadRequest_WhenUnitIsEmpty() [Fact] public async Task PutMatrix_ShouldSetResponsible_WhenSettingDepartment() { - const string department = "PDP PRD FE ANE ANE5"; + const string department = "PDP PRD FEF ANE ANE5"; var resourceOwner = fixture.AddResourceOwner(department); @@ -143,7 +143,7 @@ public async Task PutMatrix_ShouldSetResponsible_WhenSettingDepartment() LocationId = Guid.NewGuid(), Discipline = "WallaWallaUpdated", BasePositionId = testProject.Positions.First().BasePosition.Id, - Sector = "PRD FE ANE", + Sector = "PRD FEF ANE", Unit = department, }; diff --git a/src/backend/tests/Fusion.Testing.Mocks.LineOrgService/LineOrgServiceMock.cs b/src/backend/tests/Fusion.Testing.Mocks.LineOrgService/LineOrgServiceMock.cs index f103167ff0..ddc0e8f236 100644 --- a/src/backend/tests/Fusion.Testing.Mocks.LineOrgService/LineOrgServiceMock.cs +++ b/src/backend/tests/Fusion.Testing.Mocks.LineOrgService/LineOrgServiceMock.cs @@ -9,6 +9,7 @@ using System.Collections.Concurrent; using System.Linq; using System.Net.Http; +using static Microsoft.ApplicationInsights.MetricDimensionNames.TelemetryContext; namespace Fusion.Testing.Mocks.LineOrgService { @@ -108,6 +109,36 @@ public static ApiOrgUnit AddOrgUnit(string fullDepartment) return AddOrgUnit($"{sapId}", fullDepartment, name.GetShortName(), fullDepartment, fullDepartment.Split(' ').LastOrDefault()); } + public static void AddOrgUnitManager(string fullDepartment, ApiPersonProfileV3 user) + { + var orgUnit = LineOrgServiceMock.AddOrgUnit(fullDepartment); + + // Add user to the management list for the org unit + if (orgUnit.Management is null) + { + orgUnit.Management = new ApiOrgUnitManagement() + { + Persons = new System.Collections.Generic.List() + }; + } + + orgUnit.Management.Persons.Add(new ApiPerson + { + Name = user.Name, + AzureUniqueId = user.AzureUniqueId.Value, + FullDepartment = user.FullDepartment, + Department = user.Department, + Mail = user.Mail, + Upn = user.Mail, + JobTitle = user.JobTitle, + ManagerAzureUniqueId = user.ManagerAzureUniqueId, + AccountType = $"{user.AccountType}", + AccountClassification = $"{user.AccountClassification}", + MobilePhone = user.MobilePhone, + OfficeLocation = user.OfficeLocation, + }); + + } } public class FusionTestUserBuilder diff --git a/src/backend/tests/Fusion.Testing.Mocks.ProfileService/Fusion.Testing.Mocks.ProfileService.csproj b/src/backend/tests/Fusion.Testing.Mocks.ProfileService/Fusion.Testing.Mocks.ProfileService.csproj index a5ef9e50b3..1e22aacade 100644 --- a/src/backend/tests/Fusion.Testing.Mocks.ProfileService/Fusion.Testing.Mocks.ProfileService.csproj +++ b/src/backend/tests/Fusion.Testing.Mocks.ProfileService/Fusion.Testing.Mocks.ProfileService.csproj @@ -6,13 +6,14 @@ - + - - + + - + + diff --git a/src/backend/tests/Fusion.Testing.Mocks.ProfileService/PeopleServiceMock.cs b/src/backend/tests/Fusion.Testing.Mocks.ProfileService/PeopleServiceMock.cs index d129e91184..419ad5bf1b 100644 --- a/src/backend/tests/Fusion.Testing.Mocks.ProfileService/PeopleServiceMock.cs +++ b/src/backend/tests/Fusion.Testing.Mocks.ProfileService/PeopleServiceMock.cs @@ -8,6 +8,10 @@ using System.Collections.Generic; using System.Linq; using System.Net.Http; +using static System.Net.Mime.MediaTypeNames; +using System.Threading.Tasks; +using HashLib; +using HashLib.Checksum; namespace Fusion.Testing.Mocks.ProfileService { @@ -58,9 +62,59 @@ public FusionTestUserBuilder WithPreferredContactMail(string mail) return this; } + /// + /// Will flag the profile as resource owner by setting the bool property to true. + /// The profile will also be added to the department above or CEC. + /// + /// A LineOrg role will be added as well. + /// + /// Should be called after the full department has been updated. + /// + /// public FusionTestUserBuilder AsResourceOwner() { profile.IsResourceOwner = true; + + try + { + var depTokens = profile.FullDepartment.Split(' '); + // Lift manager 1 level up. + + var parentFullDepartment = string.Join(" ", depTokens.SkipLast(1)); + var parentDepartment = string.Join(" ", depTokens.SkipLast(1).TakeLast(3)); + + if (string.IsNullOrEmpty(parentDepartment)) + parentDepartment = "CEC"; + if (string.IsNullOrEmpty(parentFullDepartment)) + parentFullDepartment = "CEC"; + + profile.FullDepartment = parentFullDepartment; + profile.Department = parentDepartment; + + } + catch (Exception) { /* */ } + + // Must add roles.. Create SAP id + if (profile.Roles is null) + profile.Roles = new List(); + + // Generate sapId same way as lineorg mock + var hash = HashFactory.Checksum.CreateCRC32(0xF0F0F0F0); + var hashResult = hash.ComputeString(profile.FullDepartment); + var sapId = $"{Math.Abs(hashResult.GetInt())}"; + + + profile.Roles = new List + { + new ApiPersonRoleV3 + { + Name = "Fusion.LineOrg.Manager", + Scope = new ApiPersonRoleScopeV3 { Type = "OrgUnit", Value = sapId }, + IsActive = true, + OnDemandSupport = false + } + }; + return this; } public FusionTestUserBuilder WithAccountType(FusionAccountType type) @@ -93,6 +147,13 @@ public FusionTestUserBuilder WithDepartment(string department) return this; } + public FusionTestUserBuilder WithPositions(IEnumerable positions) + { + foreach (var position in positions) + WithPosition(position); + return this; + } + public FusionTestUserBuilder WithPosition(ApiPositionV2 position) { position.Instances.ForEach(instance => profile.Positions.Add(new ApiPersonPositionV3