diff --git a/.changeset/pr-899-2238967603.md b/.changeset/pr-899-2238967603.md new file mode 100644 index 00000000..67ebd20d --- /dev/null +++ b/.changeset/pr-899-2238967603.md @@ -0,0 +1,5 @@ + +--- +"fusion-project-portal": minor +--- +- Added OPTIONS endpoints for most API resources diff --git a/backend/src/Equinor.ProjectExecutionPortal.Application/Services/PortalService/PortalService.cs b/backend/src/Equinor.ProjectExecutionPortal.Application/Services/PortalService/PortalService.cs index 71a7301e..d5ed0948 100644 --- a/backend/src/Equinor.ProjectExecutionPortal.Application/Services/PortalService/PortalService.cs +++ b/backend/src/Equinor.ProjectExecutionPortal.Application/Services/PortalService/PortalService.cs @@ -53,6 +53,7 @@ public PortalOnboardedAppDto SetAppAsActiveInPortal(PortalOnboardedAppDto app) public async Task UserIsAdmin(Guid portalId, Guid userOId) { var isAdmin = await _context.Set() + .AsNoTracking() .Include(portal => portal.Admins) .Where(portal => portal.Id == portalId) .AnyAsync(x => x.Admins.Any(o => o.AzureUniqueId == userOId)); diff --git a/backend/src/Equinor.ProjectExecutionPortal.WebApi/Authorization/Extensions/AuthorizationServiceExtensions.cs b/backend/src/Equinor.ProjectExecutionPortal.WebApi/Authorization/Extensions/AuthorizationServiceExtensions.cs new file mode 100644 index 00000000..eb09123f --- /dev/null +++ b/backend/src/Equinor.ProjectExecutionPortal.WebApi/Authorization/Extensions/AuthorizationServiceExtensions.cs @@ -0,0 +1,14 @@ +using Microsoft.AspNetCore.Authorization; + +namespace Equinor.ProjectExecutionPortal.WebApi.Authorization.Extensions; + +public static class AuthorizationServiceExtensions +{ + public static async Task RequireAuthorizationAsync(this HttpRequest request, object? resource, string policy) + { + var authorizationService = request.HttpContext.RequestServices.GetRequiredService(); + var user = request.HttpContext.User; + + return await authorizationService.AuthorizeAsync(user, resource, policy); + } +} diff --git a/backend/src/Equinor.ProjectExecutionPortal.WebApi/Authorization/Extensions/RequirementBuilderExtensions.cs b/backend/src/Equinor.ProjectExecutionPortal.WebApi/Authorization/Extensions/RequirementBuilderExtensions.cs deleted file mode 100644 index baf16e8b..00000000 --- a/backend/src/Equinor.ProjectExecutionPortal.WebApi/Authorization/Extensions/RequirementBuilderExtensions.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Equinor.ProjectExecutionPortal.WebApi.Authorization.Requirements; -using Fusion.AspNetCore.FluentAuthorization; -using Microsoft.AspNetCore.Authorization; - -namespace Equinor.ProjectExecutionPortal.WebApi.Authorization.Extensions; - -public static class ApplicationsRequirementBuilderExtensions -{ - public static IAuthorizationRequirementRule HasPortalsFullControl(this IAuthorizationRequirementRule builder) - { - builder.OrGlobalRole(Scopes.ProjectPortalAdmin); - - return builder; - } - - public static IAuthorizationRequirementRule BePortalAdmin(this IAuthorizationRequirementRule builder, Guid portalId) - { - builder.AddRule(portalId, new PortalAdminRequirement()); - - return builder; - } - - private static IAuthorizationRequirementRule OrGlobalRole(this IAuthorizationRequirementRule builder, params string[] scopes) - { - var policy = new AuthorizationPolicyBuilder() - .RequireAssertion(c => scopes.Any(role => c.User.IsInRole(role))) - .Build(); - - builder.AddRule((auth, user) => auth.AuthorizeAsync(user, policy)); - - return builder; - } -} diff --git a/backend/src/Equinor.ProjectExecutionPortal.WebApi/Authorization/Policies.cs b/backend/src/Equinor.ProjectExecutionPortal.WebApi/Authorization/Policies.cs new file mode 100644 index 00000000..91bf06d3 --- /dev/null +++ b/backend/src/Equinor.ProjectExecutionPortal.WebApi/Authorization/Policies.cs @@ -0,0 +1,26 @@ +using Equinor.ProjectExecutionPortal.WebApi.Authorization.Requirements; +using Microsoft.AspNetCore.Authorization; + +namespace Equinor.ProjectExecutionPortal.WebApi.Authorization; + +public static class Policies +{ + public static class Global + { + public const string Read = "Global.Read"; + public const string ManagePortal = "Global.ManagePortal"; + public const string Administrate = "Global.Administrate"; + } + + public static void UseApplicationPolicies(this AuthorizationOptions options) + { + options.AddPolicy(Global.Read, builder => builder + .RequireAuthenticatedUser()); + + options.AddPolicy(Global.ManagePortal, builder => builder + .AddRequirements(new PortalManageRequirement())); + + options.AddPolicy(Global.Administrate, builder => builder + .RequireRole(Scopes.ProjectPortalAdmin)); + } +} diff --git a/backend/src/Equinor.ProjectExecutionPortal.WebApi/Authorization/Requirements/PortalAdminRequirement.cs b/backend/src/Equinor.ProjectExecutionPortal.WebApi/Authorization/Requirements/PortalManageRequirement.cs similarity index 56% rename from backend/src/Equinor.ProjectExecutionPortal.WebApi/Authorization/Requirements/PortalAdminRequirement.cs rename to backend/src/Equinor.ProjectExecutionPortal.WebApi/Authorization/Requirements/PortalManageRequirement.cs index ab4b9cb5..abb342a0 100644 --- a/backend/src/Equinor.ProjectExecutionPortal.WebApi/Authorization/Requirements/PortalAdminRequirement.cs +++ b/backend/src/Equinor.ProjectExecutionPortal.WebApi/Authorization/Requirements/PortalManageRequirement.cs @@ -5,12 +5,12 @@ namespace Equinor.ProjectExecutionPortal.WebApi.Authorization.Requirements; -public class PortalAdminRequirement : FusionAuthorizationRequirement +public class PortalManageRequirement : FusionAuthorizationRequirement { - public override string Description => "User must be an portal admin"; + public override string Description => "User must be either a portal admin or global admin"; public override string Code => "PortalAdmins"; - public class Handler : AuthorizationHandler + public class Handler : AuthorizationHandler { private readonly IPortalService _portalService; @@ -19,13 +19,20 @@ public Handler(IPortalService portalService) _portalService = portalService; } - protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, PortalAdminRequirement requirement, Guid portalId) + protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, PortalManageRequirement requirement, Guid portalId) { var userOId = context.User.GetAzureUniqueIdOrThrow(); - var isAdmin = await _portalService.UserIsAdmin(portalId, userOId); + var isGlobalAdmin = context.User.IsInRole(Scopes.ProjectPortalAdmin); - if (isAdmin) + if (isGlobalAdmin) + { + context.Succeed(requirement); + } + + var isPortalAdmin = await _portalService.UserIsAdmin(portalId, userOId); + + if (isPortalAdmin) { context.Succeed(requirement); } diff --git a/backend/src/Equinor.ProjectExecutionPortal.WebApi/Controllers/ApiControllerBase.cs b/backend/src/Equinor.ProjectExecutionPortal.WebApi/Controllers/ApiControllerBase.cs index 8cbb34c4..13f34821 100644 --- a/backend/src/Equinor.ProjectExecutionPortal.WebApi/Controllers/ApiControllerBase.cs +++ b/backend/src/Equinor.ProjectExecutionPortal.WebApi/Controllers/ApiControllerBase.cs @@ -1,5 +1,6 @@ using Fusion.Integration; using MediatR; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace Equinor.ProjectExecutionPortal.WebApi.Controllers; @@ -7,9 +8,39 @@ namespace Equinor.ProjectExecutionPortal.WebApi.Controllers; [ApiController] public abstract class ApiControllerBase : Controller { - private ISender? _mediator; - private IFusionContextResolver? _contextResolver; + private IAuthorizationService _authorizationService = null!; + private IFusionContextResolver _contextResolver = null!; + private ISender _mediator = null!; protected ISender Mediator => _mediator ??= HttpContext.RequestServices.GetRequiredService(); protected IFusionContextResolver ContextResolver => _contextResolver ??= HttpContext.RequestServices.GetRequiredService(); + protected IAuthorizationService AuthorizationService => _authorizationService ??= HttpContext.RequestServices.GetRequiredService(); + + public static ActionResult CreateForbiddenResponse() + { + return FusionApiError.Forbidden("User is not authorized"); + } + + private protected async Task SetAuthorizedVerbsHeader(List<(string verb, string policy)> verbPolicyMap, object? resource) + { + var allowedVerbs = await GetAuthorizedVerbs(verbPolicyMap, resource); + HttpContext.Response.Headers.Append("Allow", string.Join(',', allowedVerbs)); + } + + private async Task> GetAuthorizedVerbs(List<(string verb, string policy)> verbPolicyMap, object? resource) + { + var allowedVerbs = new List { HttpMethod.Options.Method }; // Always allowed + + foreach (var (verb, policy) in verbPolicyMap) + { + var authResult = await AuthorizationService.AuthorizeAsync(User, resource, policy); + + if (authResult.Succeeded) + { + allowedVerbs.Add(verb); + } + } + + return allowedVerbs; + } } diff --git a/backend/src/Equinor.ProjectExecutionPortal.WebApi/Controllers/ContextTypeController.cs b/backend/src/Equinor.ProjectExecutionPortal.WebApi/Controllers/ContextTypeController.cs index a88355d1..2c197673 100644 --- a/backend/src/Equinor.ProjectExecutionPortal.WebApi/Controllers/ContextTypeController.cs +++ b/backend/src/Equinor.ProjectExecutionPortal.WebApi/Controllers/ContextTypeController.cs @@ -1,9 +1,9 @@ using System.Net.Mime; using Equinor.ProjectExecutionPortal.Application.Queries.ContextTypes.GetContextTypes; using Equinor.ProjectExecutionPortal.Domain.Common.Exceptions; -using Equinor.ProjectExecutionPortal.WebApi.Authorization.Extensions; +using Equinor.ProjectExecutionPortal.WebApi.Authorization; using Equinor.ProjectExecutionPortal.WebApi.ViewModels.ContextType; -using Fusion.AspNetCore.FluentAuthorization; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace Equinor.ProjectExecutionPortal.WebApi.Controllers; @@ -12,6 +12,7 @@ namespace Equinor.ProjectExecutionPortal.WebApi.Controllers; [Route("api/context-types")] public class ContextTypeController : ApiControllerBase { + [Authorize(Policy = Policies.Global.Read)] [HttpGet("")] [Produces(MediaTypeNames.Application.Json)] [ProducesResponseType(StatusCodes.Status200OK)] @@ -23,6 +24,7 @@ public async Task>> GetContextTypes() return Ok(contextTypesDto.Select(contextTypeDto => new ApiContextType(contextTypeDto)).ToList()); } + [Authorize(Policy = Policies.Global.Administrate)] [HttpPost("")] [Consumes(MediaTypeNames.Application.Json)] [Produces(MediaTypeNames.Application.Json)] @@ -32,20 +34,6 @@ public async Task>> GetContextTypes() [ProducesResponseType(typeof(void), StatusCodes.Status409Conflict)] public async Task> AddContextType([FromBody] ApiAddContextTypeRequest request) { - #region Authorization - - var authResult = await Request.RequireAuthorizationAsync(builder => - { - builder.AlwaysAccessWhen().HasPortalsFullControl(); - }); - - if (authResult.Unauthorized) - { - return authResult.CreateForbiddenResponse(); - } - - #endregion - try { await Mediator.Send(request.ToCommand()); @@ -66,6 +54,7 @@ public async Task> AddContextType([FromBody] ApiAddContextTyp return Created("Created", request); } + [Authorize(Policy = Policies.Global.Administrate)] [HttpDelete("{contextType}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)] @@ -73,20 +62,6 @@ public async Task> AddContextType([FromBody] ApiAddContextTyp [ProducesResponseType(typeof(void), StatusCodes.Status400BadRequest)] public async Task RemoveContextType([FromRoute] string contextType) { - #region Authorization - - var authResult = await Request.RequireAuthorizationAsync(builder => - { - builder.AlwaysAccessWhen().HasPortalsFullControl(); - }); - - if (authResult.Unauthorized) - { - return authResult.CreateForbiddenResponse(); - } - - #endregion - try { await Mediator.Send(new ApiRemoveContextTypeRequest { Type = contextType }.ToCommand()); @@ -106,4 +81,19 @@ public async Task RemoveContextType([FromRoute] string contextType return Ok(); } + + [HttpOptions] + public async Task ContextTypesOptions() + { + var verbPolicyMap = new List<(string verb, string policy)> + { + (HttpMethod.Get.Method, Policies.Global.Read), + (HttpMethod.Post.Method, Policies.Global.Administrate), + (HttpMethod.Delete.Method, Policies.Global.Administrate) + }; + + await SetAuthorizedVerbsHeader(verbPolicyMap, null); + + return NoContent(); + } } diff --git a/backend/src/Equinor.ProjectExecutionPortal.WebApi/Controllers/OnboardedAppController.cs b/backend/src/Equinor.ProjectExecutionPortal.WebApi/Controllers/OnboardedAppController.cs index d1926326..b118927e 100644 --- a/backend/src/Equinor.ProjectExecutionPortal.WebApi/Controllers/OnboardedAppController.cs +++ b/backend/src/Equinor.ProjectExecutionPortal.WebApi/Controllers/OnboardedAppController.cs @@ -1,12 +1,12 @@ -using System.Net.Mime; -using Equinor.ProjectExecutionPortal.Application.Queries.OnboardedApps.GetOnboardedApp; +using Equinor.ProjectExecutionPortal.Application.Queries.OnboardedApps.GetOnboardedApp; using Equinor.ProjectExecutionPortal.Application.Queries.OnboardedApps.GetOnboardedApps; using Equinor.ProjectExecutionPortal.Domain.Common.Exceptions; -using Equinor.ProjectExecutionPortal.WebApi.Authorization.Extensions; +using Equinor.ProjectExecutionPortal.WebApi.Authorization; using Equinor.ProjectExecutionPortal.WebApi.ViewModels.OnboardedApp; using Equinor.ProjectExecutionPortal.WebApi.ViewModels.OnboardedAppContextType; -using Fusion.AspNetCore.FluentAuthorization; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using System.Net.Mime; namespace Equinor.ProjectExecutionPortal.WebApi.Controllers; @@ -14,6 +14,7 @@ namespace Equinor.ProjectExecutionPortal.WebApi.Controllers; [Route("api/onboarded-apps")] public class OnboardedAppController : ApiControllerBase { + [Authorize(Policy = Policies.Global.Read)] [HttpGet("")] public async Task>> GetOnboardedApps() { @@ -22,6 +23,7 @@ public async Task>> GetOnboardedApps() return Ok(onboardedAppsDto.Select(onboardedAppDto => new ApiOnboardedApp(onboardedAppDto)).ToList()); } + [Authorize(Policy = Policies.Global.Read)] [HttpGet("{appKey}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(typeof(void), StatusCodes.Status404NotFound)] @@ -37,6 +39,7 @@ public async Task> GetOnboardedApp([FromRo return new ApiOnboardedAppExpanded(onboardedAppDto); } + [Authorize(Policy = Policies.Global.Administrate)] [HttpPost("")] [Consumes(MediaTypeNames.Application.Json)] [Produces(MediaTypeNames.Application.Json)] @@ -47,20 +50,6 @@ public async Task> GetOnboardedApp([FromRo [ProducesResponseType(typeof(void), StatusCodes.Status400BadRequest)] public async Task> OnboardApp([FromBody] ApiOnboardAppRequest request) { - #region Authorization - - var authResult = await Request.RequireAuthorizationAsync(builder => - { - builder.AlwaysAccessWhen().HasPortalsFullControl(); - }); - - if (authResult.Unauthorized) - { - return authResult.CreateForbiddenResponse(); - } - - #endregion - try { await Mediator.Send(request.ToCommand()); @@ -85,6 +74,7 @@ public async Task> OnboardApp([FromBody] ApiOnboardAppRequest return Created(); } + [Authorize(Policy = Policies.Global.Administrate)] [HttpPut("{appKey}")] [Consumes(MediaTypeNames.Application.Json)] [Produces(MediaTypeNames.Application.Json)] @@ -94,20 +84,6 @@ public async Task> OnboardApp([FromBody] ApiOnboardAppRequest [ProducesResponseType(typeof(void), StatusCodes.Status400BadRequest)] public async Task> UpdateOnboardedApp([FromRoute] string appKey, [FromBody] ApiUpdateOnboardedAppRequest request) { - #region Authorization - - var authResult = await Request.RequireAuthorizationAsync(builder => - { - builder.AlwaysAccessWhen().HasPortalsFullControl(); - }); - - if (authResult.Unauthorized) - { - return authResult.CreateForbiddenResponse(); - } - - #endregion - try { await Mediator.Send(request.ToCommand(appKey)); @@ -128,6 +104,7 @@ public async Task> UpdateOnboardedApp([FromRoute] string appK return Ok(); } + [Authorize(Policy = Policies.Global.Administrate)] [HttpDelete("{appKey}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)] @@ -135,20 +112,6 @@ public async Task> UpdateOnboardedApp([FromRoute] string appK [ProducesResponseType(typeof(void), StatusCodes.Status400BadRequest)] public async Task RemoveOnboardedApp([FromRoute] string appKey) { - #region Authorization - - var authResult = await Request.RequireAuthorizationAsync(builder => - { - builder.AlwaysAccessWhen().HasPortalsFullControl(); - }); - - if (authResult.Unauthorized) - { - return authResult.CreateForbiddenResponse(); - } - - #endregion - try { await Mediator.Send(new ApiRemoveOnboardedAppRequest { AppKey = appKey }.ToCommand()); @@ -171,6 +134,7 @@ public async Task RemoveOnboardedApp([FromRoute] string appKey) // ContextTypes + [Authorize(Policy = Policies.Global.Administrate)] [HttpPost("{appKey}/context-type")] [Consumes(MediaTypeNames.Application.Json)] [Produces(MediaTypeNames.Application.Json)] @@ -181,20 +145,6 @@ public async Task RemoveOnboardedApp([FromRoute] string appKey) [ProducesResponseType(typeof(void), StatusCodes.Status400BadRequest)] public async Task> AddContextTypeToOnboardedApp([FromRoute] string appKey, [FromBody] ApiAddContextTypeToOnboardedAppRequest request) { - #region Authorization - - var authResult = await Request.RequireAuthorizationAsync(builder => - { - builder.AlwaysAccessWhen().HasPortalsFullControl(); - }); - - if (authResult.Unauthorized) - { - return authResult.CreateForbiddenResponse(); - } - - #endregion - try { await Mediator.Send(request.ToCommand(appKey)); @@ -219,6 +169,7 @@ public async Task> AddContextTypeToOnboardedApp([FromRoute] s return Ok(); } + [Authorize(Policy = Policies.Global.Administrate)] [HttpDelete("{appKey}/context-type/{contextType}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)] @@ -226,20 +177,6 @@ public async Task> AddContextTypeToOnboardedApp([FromRoute] s [ProducesResponseType(typeof(void), StatusCodes.Status400BadRequest)] public async Task RemoveContextTypeFromOnboardedApp([FromRoute] string appKey, [FromRoute] string contextType) { - #region Authorization - - var authResult = await Request.RequireAuthorizationAsync(builder => - { - builder.AlwaysAccessWhen().HasPortalsFullControl(); - }); - - if (authResult.Unauthorized) - { - return authResult.CreateForbiddenResponse(); - } - - #endregion - try { await Mediator.Send(new ApiRemoveOnboardedAppContextType().ToCommand(appKey, contextType)); @@ -259,4 +196,20 @@ public async Task RemoveContextTypeFromOnboardedApp([FromRoute] st return Ok(); } + + [HttpOptions] + public async Task OnboardedAppsOptions() + { + var verbPolicyMap = new List<(string verb, string policy)> + { + (HttpMethod.Get.Method, Policies.Global.Read), + (HttpMethod.Post.Method, Policies.Global.Administrate), + (HttpMethod.Put.Method, Policies.Global.Administrate), + (HttpMethod.Delete.Method, Policies.Global.Administrate) + }; + + await SetAuthorizedVerbsHeader(verbPolicyMap, null); + + return NoContent(); + } } diff --git a/backend/src/Equinor.ProjectExecutionPortal.WebApi/Controllers/OnboardedContextController.cs b/backend/src/Equinor.ProjectExecutionPortal.WebApi/Controllers/OnboardedContextController.cs index d2c56214..0461e35f 100644 --- a/backend/src/Equinor.ProjectExecutionPortal.WebApi/Controllers/OnboardedContextController.cs +++ b/backend/src/Equinor.ProjectExecutionPortal.WebApi/Controllers/OnboardedContextController.cs @@ -1,12 +1,12 @@ -using System.Net.Mime; -using Equinor.ProjectExecutionPortal.Application.Queries.OnboardedContexts.GetOnboardedContext; +using Equinor.ProjectExecutionPortal.Application.Queries.OnboardedContexts.GetOnboardedContext; using Equinor.ProjectExecutionPortal.Application.Queries.OnboardedContexts.GetOnboardedContexts; using Equinor.ProjectExecutionPortal.Domain.Common.Exceptions; -using Equinor.ProjectExecutionPortal.WebApi.Authorization.Extensions; +using Equinor.ProjectExecutionPortal.WebApi.Authorization; using Equinor.ProjectExecutionPortal.WebApi.ViewModels.OnboardedContext; -using Fusion.AspNetCore.FluentAuthorization; using Fusion.Integration; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using System.Net.Mime; namespace Equinor.ProjectExecutionPortal.WebApi.Controllers; @@ -52,6 +52,7 @@ public async Task> GetOnboardedContext([FromRo return Ok(new ApiOnboardedContext(onboardedContext)); } + [Authorize(Policy = Policies.Global.Administrate)] [HttpPost("")] [Consumes(MediaTypeNames.Application.Json)] [Produces(MediaTypeNames.Application.Json)] @@ -62,20 +63,6 @@ public async Task> GetOnboardedContext([FromRo [ProducesResponseType(typeof(void), StatusCodes.Status400BadRequest)] public async Task> OnboardContext([FromBody] ApiOnboardContextRequest request) { - #region Authorization - - var authResult = await Request.RequireAuthorizationAsync(builder => - { - builder.AlwaysAccessWhen().HasPortalsFullControl(); - }); - - if (authResult.Unauthorized) - { - return authResult.CreateForbiddenResponse(); - } - - #endregion - var contextIdentifier = ContextIdentifier.FromExternalId(request.ExternalId); var context = await ContextResolver.ResolveContextAsync(contextIdentifier, request.Type); @@ -100,6 +87,7 @@ public async Task> OnboardContext([FromBody] ApiOnboardCont return Ok(); } + [Authorize(Policy = Policies.Global.Administrate)] [HttpPut("{id:guid}")] [Consumes(MediaTypeNames.Application.Json)] [Produces(MediaTypeNames.Application.Json)] @@ -109,20 +97,6 @@ public async Task> OnboardContext([FromBody] ApiOnboardCont [ProducesResponseType(typeof(void), StatusCodes.Status400BadRequest)] public async Task> UpdateOnboardedContext([FromRoute] Guid id, [FromBody] ApiUpdateOnboardedContextRequest request) { - #region Authorization - - var authResult = await Request.RequireAuthorizationAsync(builder => - { - builder.AlwaysAccessWhen().HasPortalsFullControl(); - }); - - if (authResult.Unauthorized) - { - return authResult.CreateForbiddenResponse(); - } - - #endregion - try { await Mediator.Send(request.ToCommand(id)); @@ -139,6 +113,7 @@ public async Task> UpdateOnboardedContext([FromRoute] Guid return Ok(); } + [Authorize(Policy = Policies.Global.Administrate)] [HttpDelete("{id:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)] @@ -146,20 +121,6 @@ public async Task> UpdateOnboardedContext([FromRoute] Guid [ProducesResponseType(typeof(void), StatusCodes.Status400BadRequest)] public async Task RemoveOnboardedContext([FromRoute] Guid id) { - #region Authorization - - var authResult = await Request.RequireAuthorizationAsync(builder => - { - builder.AlwaysAccessWhen().HasPortalsFullControl(); - }); - - if (authResult.Unauthorized) - { - return authResult.CreateForbiddenResponse(); - } - - #endregion - try { await Mediator.Send(new ApiRemoveOnboardedContextRequest { Id = id }.ToCommand()); @@ -179,4 +140,20 @@ public async Task RemoveOnboardedContext([FromRoute] Guid id) return Ok(); } + + [HttpOptions] + public async Task OnboardedContextsOptions() + { + var verbPolicyMap = new List<(string verb, string policy)> + { + (HttpMethod.Get.Method, Policies.Global.Read), + (HttpMethod.Post.Method, Policies.Global.Administrate), + (HttpMethod.Put.Method, Policies.Global.Administrate), + (HttpMethod.Delete.Method, Policies.Global.Administrate) + }; + + await SetAuthorizedVerbsHeader(verbPolicyMap, null); + + return NoContent(); + } } diff --git a/backend/src/Equinor.ProjectExecutionPortal.WebApi/Controllers/PortalController.cs b/backend/src/Equinor.ProjectExecutionPortal.WebApi/Controllers/PortalController.cs index 3ecaf8d7..9b3f00c5 100644 --- a/backend/src/Equinor.ProjectExecutionPortal.WebApi/Controllers/PortalController.cs +++ b/backend/src/Equinor.ProjectExecutionPortal.WebApi/Controllers/PortalController.cs @@ -1,17 +1,18 @@ -using System.Net.Mime; -using Equinor.ProjectExecutionPortal.Application.Queries.Portals.GetPortal; +using Equinor.ProjectExecutionPortal.Application.Queries.Portals.GetPortal; using Equinor.ProjectExecutionPortal.Application.Queries.Portals.GetPortalAppKeys; using Equinor.ProjectExecutionPortal.Application.Queries.Portals.GetPortalConfiguration; using Equinor.ProjectExecutionPortal.Application.Queries.Portals.GetPortalOnboardedApp; using Equinor.ProjectExecutionPortal.Application.Queries.Portals.GetPortalOnboardedApps; using Equinor.ProjectExecutionPortal.Application.Queries.Portals.GetPortals; using Equinor.ProjectExecutionPortal.Domain.Common.Exceptions; +using Equinor.ProjectExecutionPortal.WebApi.Authorization; using Equinor.ProjectExecutionPortal.WebApi.Authorization.Extensions; using Equinor.ProjectExecutionPortal.WebApi.ViewModels.Portal; using Equinor.ProjectExecutionPortal.WebApi.ViewModels.PortalApp; using Equinor.ProjectExecutionPortal.WebApi.ViewModels.PortalContextType; -using Fusion.AspNetCore.FluentAuthorization; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using System.Net.Mime; namespace Equinor.ProjectExecutionPortal.WebApi.Controllers; @@ -19,6 +20,7 @@ namespace Equinor.ProjectExecutionPortal.WebApi.Controllers; [Route("api/portals")] public class PortalController : ApiControllerBase { + [Authorize(Policy = Policies.Global.Read)] [HttpGet("")] public async Task>> GetPortals() { @@ -27,6 +29,7 @@ public async Task>> GetPortals() return Ok(portalDtos.Select(dto => new ApiPortal(dto)).ToList()); } + [Authorize(Policy = Policies.Global.Read)] [HttpGet("{portalId:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(typeof(void), StatusCodes.Status404NotFound)] @@ -42,6 +45,7 @@ public async Task> GetPortal([FromRoute] Guid portalId) return Ok(new ApiPortal(portalWithAppsDto)); } + [Authorize(Policy = Policies.Global.Administrate)] [HttpPost("")] [Consumes(MediaTypeNames.Application.Json)] [Produces(MediaTypeNames.Application.Json)] @@ -51,20 +55,6 @@ public async Task> GetPortal([FromRoute] Guid portalId) [ProducesResponseType(typeof(void), StatusCodes.Status400BadRequest)] public async Task> CreatePortal([FromBody] ApiCreatePortalRequest request) { - #region Authorization - - var authResult = await Request.RequireAuthorizationAsync(builder => - { - builder.AlwaysAccessWhen().HasPortalsFullControl(); - }); - - if (authResult.Unauthorized) - { - return authResult.CreateForbiddenResponse(); - } - - #endregion - try { await Mediator.Send(request.ToCommand()); @@ -94,17 +84,13 @@ public async Task> CreatePortal([FromBody] ApiCreatePortalReq [ProducesResponseType(typeof(void), StatusCodes.Status400BadRequest)] public async Task> UpdatePortal([FromRoute] Guid portalId, [FromBody] ApiUpdatePortalRequest request) { - #region Authorization - - var authResult = await AuthorizeCanManagePortal(portalId); + var authResult = await Request.RequireAuthorizationAsync(portalId, Policies.Global.ManagePortal); - if (authResult.Unauthorized) + if (!authResult.Succeeded) { - return authResult.CreateForbiddenResponse(); + return CreateForbiddenResponse(); } - #endregion - try { await Mediator.Send(request.ToCommand(portalId)); @@ -121,6 +107,7 @@ public async Task> UpdatePortal([FromRoute] Guid portalId, [F return Ok(); } + [Authorize(Policy = Policies.Global.Read)] [HttpGet("{portalId:guid}/configuration")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(typeof(void), StatusCodes.Status404NotFound)] @@ -145,17 +132,13 @@ public async Task> GetPortalConfiguration([ [ProducesResponseType(typeof(void), StatusCodes.Status400BadRequest)] public async Task> UpdatePortalConfiguration([FromRoute] Guid portalId, [FromBody] ApiUpdatePortalConfigurationRequest request) { - #region Authorization - - var authResult = await AuthorizeCanManagePortal(portalId); + var authResult = await Request.RequireAuthorizationAsync(portalId, Policies.Global.ManagePortal); - if (authResult.Unauthorized) + if (!authResult.Succeeded) { - return authResult.CreateForbiddenResponse(); + return CreateForbiddenResponse(); } - #endregion - try { await Mediator.Send(request.ToCommand(portalId)); @@ -172,6 +155,7 @@ public async Task> UpdatePortalConfiguration([FromRoute] Guid return Ok(); } + [Authorize(Policy = Policies.Global.Administrate)] [HttpDelete("{portalId:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)] @@ -179,24 +163,9 @@ public async Task> UpdatePortalConfiguration([FromRoute] Guid [ProducesResponseType(typeof(void), StatusCodes.Status400BadRequest)] public async Task RemovePortal([FromRoute] Guid portalId) { - #region Authorization - - var authResult = await Request.RequireAuthorizationAsync(builder => - { - builder.AlwaysAccessWhen().HasPortalsFullControl(); - }); - - if (authResult.Unauthorized) - { - return authResult.CreateForbiddenResponse(); - } - - #endregion - - var request = new ApiRemovePortalRequest(); - try { + var request = new ApiRemovePortalRequest(); await Mediator.Send(request.ToCommand(portalId)); } catch (NotFoundException ex) @@ -217,6 +186,7 @@ public async Task RemovePortal([FromRoute] Guid portalId) // Onboarded Apps + [Authorize(Policy = Policies.Global.Read)] [HttpGet("{portalId:guid}/onboarded-apps")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(typeof(void), StatusCodes.Status404NotFound)] @@ -232,6 +202,7 @@ public async Task>> GetOnboardedAppsFor return Ok(portalOnboardedAppsDto.Select(onboardedAppDto => new ApiPortalOnboardedApp(onboardedAppDto)).ToList()); } + [Authorize(Policy = Policies.Global.Read)] [HttpGet("{portalId:guid}/onboarded-apps/{appKey}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(typeof(void), StatusCodes.Status404NotFound)] @@ -249,6 +220,7 @@ public async Task> GetOnboardedAppForPortal( // Apps + [Authorize(Policy = Policies.Global.Read)] [HttpGet("{portalId:guid}/apps")] [HttpGet("{portalId:guid}/contexts/{contextId:guid}/apps")] [ProducesResponseType(StatusCodes.Status200OK)] @@ -288,17 +260,13 @@ public async Task>> GetAppKeysForPortal([FromRoute] Gu [ProducesResponseType(typeof(void), StatusCodes.Status400BadRequest)] public async Task> AddAppToPortal([FromRoute] Guid portalId, [FromBody] ApiAddGlobalAppToPortalRequest request) { - #region Authorization + var authResult = await Request.RequireAuthorizationAsync(portalId, Policies.Global.ManagePortal); - var authResult = await AuthorizeCanManagePortal(portalId); - - if (authResult.Unauthorized) + if (!authResult.Succeeded) { - return authResult.CreateForbiddenResponse(); + return CreateForbiddenResponse(); } - #endregion - try { await Mediator.Send(request.ToCommand(portalId)); @@ -329,17 +297,13 @@ public async Task> AddAppToPortal([FromRoute] Guid portalId, [ProducesResponseType(typeof(void), StatusCodes.Status400BadRequest)] public async Task> AddAppToPortal([FromRoute] Guid portalId, Guid contextId, [FromBody] ApiAddContextAppToPortalRequest request) { - #region Authorization - - var authResult = await AuthorizeCanManagePortal(portalId); + var authResult = await Request.RequireAuthorizationAsync(portalId, Policies.Global.ManagePortal); - if (authResult.Unauthorized) + if (!authResult.Succeeded) { - return authResult.CreateForbiddenResponse(); + return CreateForbiddenResponse(); } - #endregion - try { await Mediator.Send(request.ToCommand(portalId, contextId)); @@ -372,17 +336,13 @@ public async Task> AddAppToPortal([FromRoute] Guid portalId, [ProducesResponseType(typeof(void), StatusCodes.Status400BadRequest)] public async Task RemoveAppFromPortal([FromRoute] Guid portalId, [FromRoute] Guid? contextId, [FromRoute] string appKey) { - #region Authorization - - var authResult = await AuthorizeCanManagePortal(portalId); + var authResult = await Request.RequireAuthorizationAsync(portalId, Policies.Global.ManagePortal); - if (authResult.Unauthorized) + if (!authResult.Succeeded) { - return authResult.CreateForbiddenResponse(); + return CreateForbiddenResponse(); } - #endregion - try { if (contextId != null) @@ -418,17 +378,13 @@ public async Task RemoveAppFromPortal([FromRoute] Guid portalId, [ [ProducesResponseType(typeof(void), StatusCodes.Status400BadRequest)] public async Task> AddContextTypeToPortal([FromRoute] Guid portalId, [FromBody] ApiAddContextTypeToPortalRequest request) { - #region Authorization - - var authResult = await AuthorizeCanManagePortal(portalId); + var authResult = await Request.RequireAuthorizationAsync(portalId, Policies.Global.ManagePortal); - if (authResult.Unauthorized) + if (!authResult.Succeeded) { - return authResult.CreateForbiddenResponse(); + return CreateForbiddenResponse(); } - #endregion - try { await Mediator.Send(request.ToCommand(portalId)); @@ -460,20 +416,17 @@ public async Task> AddContextTypeToPortal([FromRoute] Guid po [ProducesResponseType(typeof(void), StatusCodes.Status400BadRequest)] public async Task RemoveContextTypeFromPortal([FromRoute] Guid portalId, [FromRoute] string contextType) { - #region Authorization - - var authResult = await AuthorizeCanManagePortal(portalId); + var authResult = await Request.RequireAuthorizationAsync(portalId, Policies.Global.ManagePortal); - if (authResult.Unauthorized) + if (!authResult.Succeeded) { - return authResult.CreateForbiddenResponse(); + return CreateForbiddenResponse(); } - #endregion - - var request = new ApiRemovePortalContextType(); try { + var request = new ApiRemovePortalContextType(); + await Mediator.Send(request.ToCommand(portalId, contextType)); } catch (NotFoundException ex) @@ -492,18 +445,32 @@ public async Task RemoveContextTypeFromPortal([FromRoute] Guid por return Ok(); } - private async Task AuthorizeCanManagePortal(Guid portalId) + [HttpOptions] + public async Task PortalsOptions() { - var authResult = await Request.RequireAuthorizationAsync(builder => + var verbPolicyMap = new List<(string verb, string policy)> { - builder.AlwaysAccessWhen().HasPortalsFullControl(); + (HttpMethod.Get.Method, Policies.Global.Read), + (HttpMethod.Post.Method, Policies.Global.Administrate) + }; - builder.AnyOf(or => - { - or.BePortalAdmin(portalId); - }); - }); + await SetAuthorizedVerbsHeader(verbPolicyMap, null); + + return NoContent(); + } + + [HttpOptions("{portalId:guid}")] + public async Task PortalOptions(Guid portalId) + { + var verbPolicyMap = new List<(string verb, string policy)> + { + (HttpMethod.Get.Method, Policies.Global.Read), + (HttpMethod.Put.Method, Policies.Global.ManagePortal), + (HttpMethod.Delete.Method, Policies.Global.ManagePortal) + }; + + await SetAuthorizedVerbsHeader(verbPolicyMap, portalId); - return authResult; + return NoContent(); } } diff --git a/backend/src/Equinor.ProjectExecutionPortal.WebApi/Controllers/ProfileController.cs b/backend/src/Equinor.ProjectExecutionPortal.WebApi/Controllers/ProfileController.cs deleted file mode 100644 index e27d4e41..00000000 --- a/backend/src/Equinor.ProjectExecutionPortal.WebApi/Controllers/ProfileController.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Equinor.ProjectExecutionPortal.WebApi.Authorization.Extensions; -using Fusion.AspNetCore.FluentAuthorization; -using Microsoft.AspNetCore.Mvc; - -namespace Equinor.ProjectExecutionPortal.WebApi.Controllers; - -[ApiVersion("1.0")] -[Route("api/profile")] -public class ProfileController : ApiControllerBase -{ - [HttpOptions("admin")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - public async Task Options() - { - #region Authorization - - var authResult = await Request.RequireAuthorizationAsync(builder => - { - builder.AlwaysAccessWhen().HasPortalsFullControl(); - }); - - if (authResult.Unauthorized) - { - return authResult.CreateForbiddenResponse(); - } - - #endregion - - return Ok(); - } -} diff --git a/backend/src/Equinor.ProjectExecutionPortal.WebApi/DiModules/ApplicationModule.cs b/backend/src/Equinor.ProjectExecutionPortal.WebApi/DiModules/ApplicationModule.cs index a5e4054a..9ce5c7c5 100644 --- a/backend/src/Equinor.ProjectExecutionPortal.WebApi/DiModules/ApplicationModule.cs +++ b/backend/src/Equinor.ProjectExecutionPortal.WebApi/DiModules/ApplicationModule.cs @@ -5,6 +5,7 @@ using Equinor.ProjectExecutionPortal.Domain.Common.Time; using Equinor.ProjectExecutionPortal.Domain.Interfaces; using Equinor.ProjectExecutionPortal.Infrastructure; +using Equinor.ProjectExecutionPortal.WebApi.Authorization; using Equinor.ProjectExecutionPortal.WebApi.Authorization.Requirements; using Equinor.ProjectExecutionPortal.WebApi.Behaviors; using Equinor.ProjectExecutionPortal.WebApi.Misc; @@ -32,6 +33,8 @@ public static void AddApplicationModules(this IServiceCollection services, IConf policy.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme); policy.RequireAuthenticatedUser(); }); + + options.UseApplicationPolicies(); }); services.AddInfrastructureModules(configuration); @@ -57,6 +60,6 @@ public static void AddApplicationModules(this IServiceCollection services, IConf services.AddScoped(); // Authorization handlers - services.AddScoped(); + services.AddScoped(); } }