Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add options endpoints #899

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/pr-899-2238967603.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@

---
"fusion-project-portal": minor
---
- Added OPTIONS endpoints for most API resources
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ public PortalOnboardedAppDto SetAppAsActiveInPortal(PortalOnboardedAppDto app)
public async Task<bool> UserIsAdmin(Guid portalId, Guid userOId)
{
var isAdmin = await _context.Set<Portal>()
.AsNoTracking()
.Include(portal => portal.Admins)
.Where(portal => portal.Id == portalId)
.AnyAsync(x => x.Admins.Any(o => o.AzureUniqueId == userOId));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using Microsoft.AspNetCore.Authorization;

namespace Equinor.ProjectExecutionPortal.WebApi.Authorization.Extensions;

public static class AuthorizationServiceExtensions
{
public static async Task<AuthorizationResult> RequireAuthorizationAsync(this HttpRequest request, object? resource, string policy)
{
var authorizationService = request.HttpContext.RequestServices.GetRequiredService<IAuthorizationService>();
var user = request.HttpContext.User;

return await authorizationService.AuthorizeAsync(user, resource, policy);
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<PortalAdminRequirement, Guid>
public class Handler : AuthorizationHandler<PortalManageRequirement, Guid>
{
private readonly IPortalService _portalService;

Expand All @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,46 @@
using Fusion.Integration;
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

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<ISender>();
protected IFusionContextResolver ContextResolver => _contextResolver ??= HttpContext.RequestServices.GetRequiredService<IFusionContextResolver>();
protected IAuthorizationService AuthorizationService => _authorizationService ??= HttpContext.RequestServices.GetRequiredService<IAuthorizationService>();

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<List<string>> GetAuthorizedVerbs(List<(string verb, string policy)> verbPolicyMap, object? resource)
{
var allowedVerbs = new List<string> { 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;
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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)]
Expand All @@ -23,6 +24,7 @@ public async Task<ActionResult<List<ApiContextType>>> GetContextTypes()
return Ok(contextTypesDto.Select(contextTypeDto => new ApiContextType(contextTypeDto)).ToList());
}

[Authorize(Policy = Policies.Global.Administrate)]
[HttpPost("")]
[Consumes(MediaTypeNames.Application.Json)]
[Produces(MediaTypeNames.Application.Json)]
Expand All @@ -32,20 +34,6 @@ public async Task<ActionResult<List<ApiContextType>>> GetContextTypes()
[ProducesResponseType(typeof(void), StatusCodes.Status409Conflict)]
public async Task<ActionResult<Guid>> 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());
Expand All @@ -66,27 +54,14 @@ public async Task<ActionResult<Guid>> AddContextType([FromBody] ApiAddContextTyp
return Created("Created", request);
}

[Authorize(Policy = Policies.Global.Administrate)]
[HttpDelete("{contextType}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
[ProducesResponseType(typeof(void), StatusCodes.Status404NotFound)]
[ProducesResponseType(typeof(void), StatusCodes.Status400BadRequest)]
public async Task<ActionResult> 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());
Expand All @@ -106,4 +81,19 @@ public async Task<ActionResult> RemoveContextType([FromRoute] string contextType

return Ok();
}

[HttpOptions]
public async Task<IActionResult> 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();
}
}
Loading