Skip to content

Commit

Permalink
petrsvihlik#192 use attribute-based authorization on controllers
Browse files Browse the repository at this point in the history
  • Loading branch information
Leon Segal committed Mar 6, 2025
1 parent 175a1f4 commit c792420
Show file tree
Hide file tree
Showing 18 changed files with 238 additions and 257 deletions.
41 changes: 41 additions & 0 deletions src/WopiHost.Abstractions/IWopiAuthorizationRequirement.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
namespace WopiHost.Abstractions;

/// <summary>
/// The Wopi resource type
/// </summary>
public enum WopiResourceType
{
/// <summary>
/// <see cref="IWopiFile"/>
/// </summary>
File,
/// <summary>
/// <see cref="IWopiFolder"/>
/// </summary>
Container
};


/// <summary>
/// Represents an authorization requirement for a given combination of resource, user, and action.
/// </summary>
/// <remarks>
/// Creates an instance of <see cref="IWopiAuthorizationRequirement"/> initialized
/// </remarks>
public interface IWopiAuthorizationRequirement
{
/// <summary>
/// Gets a permissions required for a given combination of resource, user, and action.
/// </summary>
Permission Permission { get; }

/// <summary>
/// Gets the type of the resource.
/// </summary>
WopiResourceType ResourceType { get; }

/// <summary>
/// Gets or sets the identifier of the resource.
/// </summary>
string? ResourceId { get; set; }
}
5 changes: 2 additions & 3 deletions src/WopiHost.Abstractions/IWopiSecurityHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,10 @@ public interface IWopiSecurityHandler
/// Verifies whether the given principal is authorized to perform a given operation on the given resource.
/// </summary>
/// <param name="principal">User principal object</param>
/// <param name="resourceId">Identifier of a resource</param>
/// <param name="operation">Type of operation to be performed</param>
/// <param name="requirement">Type of operation to be performed</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>TRUE if the given principal is authorized to perform a given operation on the given resource.</returns>
Task<bool> IsAuthorized(ClaimsPrincipal principal, string resourceId, WopiAuthorizationRequirement operation, CancellationToken cancellationToken = default);
Task<bool> IsAuthorized(ClaimsPrincipal principal, IWopiAuthorizationRequirement requirement, CancellationToken cancellationToken = default);

/// <summary>
/// Returns a string representation of a <see cref="SecurityToken"/>
Expand Down
18 changes: 0 additions & 18 deletions src/WopiHost.Abstractions/WopiAuthorizationRequirement.cs

This file was deleted.

1 change: 1 addition & 0 deletions src/WopiHost.Abstractions/WopiCheckFileInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,7 @@ public class WopiCheckFileInfo : IWopiHostCapabilities
/// A string value indicating the domain that the host page is sending and receiving PostMessages to and from.
/// Microsoft 365 for the web only sends outgoing PostMessages to this domain, and only listens to PostMessages from this domain.
/// </summary>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? PostMessageOrigin { get; set; }

/// <summary>
Expand Down
5 changes: 4 additions & 1 deletion src/WopiHost.Core/Controllers/ContainersController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using Microsoft.Extensions.Options;
using WopiHost.Abstractions;
using WopiHost.Core.Models;
using WopiHost.Core.Security.Authorization;

namespace WopiHost.Core.Controllers;

Expand All @@ -18,7 +19,7 @@ namespace WopiHost.Core.Controllers;
/// <param name="wopiHostOptions">WOPI Host configuration</param>
[Route("wopi/[controller]")]
public class ContainersController(
IWopiStorageProvider storageProvider,
IWopiStorageProvider storageProvider,
IWopiSecurityHandler securityHandler,
IOptions<WopiHostOptions> wopiHostOptions) : WopiControllerBase(storageProvider, securityHandler, wopiHostOptions)
{
Expand All @@ -31,6 +32,7 @@ public class ContainersController(
/// <returns></returns>
[HttpGet("{id}")]
[Produces(MediaTypeNames.Application.Json)]
[WopiAuthorize(Permission.Read, WopiResourceType.Container)]
public CheckContainerInfo GetCheckContainerInfo(string id)
{
var container = StorageProvider.GetWopiContainer(id);
Expand All @@ -48,6 +50,7 @@ public CheckContainerInfo GetCheckContainerInfo(string id)
/// <param name="id">Container identifier.</param>
/// <returns></returns>
[HttpGet("{id}/children")]
[WopiAuthorize(Permission.Read, WopiResourceType.Container)]
[Produces(MediaTypeNames.Application.Json)]
public Container EnumerateChildren(string id)
{
Expand Down
38 changes: 7 additions & 31 deletions src/WopiHost.Core/Controllers/FilesController.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using System.Net.Mime;
using System.Security.Claims;
using System.Text.Json;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Mvc;
Expand All @@ -11,7 +10,7 @@
using WopiHost.Core.Infrastructure;
using WopiHost.Core.Models;
using WopiHost.Core.Results;
using WopiHost.Core.Security;
using WopiHost.Core.Security.Authorization;

namespace WopiHost.Core.Controllers;

Expand All @@ -27,7 +26,6 @@ public class FilesController : WopiControllerBase
/// </summary>
private readonly ICobaltProcessor? cobaltProcessor;
private readonly IWopiLockProvider? lockProvider;
private readonly IAuthorizationService authorizationService;
private WopiHostCapabilities HostCapabilities => new()
{
SupportsCobalt = cobaltProcessor is not null,
Expand All @@ -45,18 +43,15 @@ public class FilesController : WopiControllerBase
/// <param name="storageProvider">Storage provider instance for retrieving files and folders.</param>
/// <param name="securityHandler">Security handler instance for performing security-related operations.</param>
/// <param name="wopiHostOptions">WOPI Host configuration</param>
/// <param name="authorizationService">An instance of authorization service capable of resource-based authorization.</param>
/// <param name="lockProvider">An instance of the lock provider.</param>
/// <param name="cobaltProcessor">An instance of a MS-FSSHTTP processor.</param>
public FilesController(
IWopiStorageProvider storageProvider,
IWopiSecurityHandler securityHandler,
IOptions<WopiHostOptions> wopiHostOptions,
IAuthorizationService authorizationService,
IWopiLockProvider? lockProvider = null,
ICobaltProcessor? cobaltProcessor = null) : base(storageProvider, securityHandler, wopiHostOptions)
{
this.authorizationService = authorizationService ?? throw new ArgumentNullException(nameof(authorizationService));
this.lockProvider = lockProvider;
this.cobaltProcessor = cobaltProcessor;
}
Expand All @@ -70,13 +65,9 @@ public FilesController(
/// <param name="cancellationToken">Cancellation token</param>
/// <returns></returns>
[HttpGet("{id}")]
[WopiAuthorize(Permission.Read, WopiResourceType.File)]
public async Task<IActionResult> GetCheckFileInfo(string id, CancellationToken cancellationToken = default)
{
if (!(await authorizationService.AuthorizeAsync(User, new FileResource(id), WopiOperations.Read)).Succeeded)
{
return Unauthorized();
}

// Get file
var file = StorageProvider.GetWopiFile(id);
if (file is null)
Expand Down Expand Up @@ -107,17 +98,12 @@ public async Task<IActionResult> GetCheckFileInfo(string id, CancellationToken c
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>FileStreamResult</returns>
[HttpGet("{id}/contents")]
[WopiAuthorize(Permission.Read, WopiResourceType.File)]
public async Task<IActionResult> GetFile(
string id,
[FromHeader(Name = WopiHeaders.MAX_EXPECTED_SIZE)] int? maximumExpectedSize = null,
CancellationToken cancellationToken = default)
{
// Check permissions
if (!(await authorizationService.AuthorizeAsync(User, new FileResource(id), WopiOperations.Read)).Succeeded)
{
return Unauthorized();
}

// Get file
var file = StorageProvider.GetWopiFile(id);
if (file is null)
Expand Down Expand Up @@ -157,20 +143,13 @@ public async Task<IActionResult> GetFile(
/// <returns>Returns <see cref="StatusCodes.Status200OK"/> if succeeded.</returns>
[HttpPut("{id}/contents")]
[HttpPost("{id}/contents")]
[WopiAuthorize(Permission.Update, WopiResourceType.File)]
public async Task<IActionResult> PutFile(
string id,
[FromHeader(Name = WopiHeaders.LOCK)] string? newLockIdentifier = null,
CancellationToken cancellationToken = default)
{
// Check permissions
var authorizationResult = await authorizationService.AuthorizeAsync(User, new FileResource(id), WopiOperations.Update);

if (!authorizationResult.Succeeded)
{
return Unauthorized();
}

// https://learn.microsoft.com/microsoft-365/cloud-storage-partner-program/online/scenarios/createnew
// https://learn.microsoft.com/en-us/microsoft-365/cloud-storage-partner-program/online/scenarios/createnew
var file = StorageProvider.GetWopiFile(id);
// If ... missing altogether, the host should respond with a 409 Conflict
if (file is null)
Expand Down Expand Up @@ -217,6 +196,7 @@ await HttpContext.Request.Body.CopyToAsync(
/// <param name="id">File identifier.</param>
/// <returns>Returns <see cref="StatusCodes.Status200OK"/> if succeeded.</returns>
[HttpPost("{id}"), WopiOverrideHeader([WopiFileOperations.PutRelativeFile])]
[WopiAuthorize(Permission.Update, WopiResourceType.File)]
public Task<IActionResult> PutRelativeFile(string id) => throw new NotImplementedException($"{nameof(PutRelativeFile)} is not implemented yet.");

/// <summary>
Expand All @@ -228,16 +208,12 @@ await HttpContext.Request.Body.CopyToAsync(
/// <param name="id">File identifier.</param>
/// <param name="correlationId"></param>
[HttpPost("{id}"), WopiOverrideHeader([WopiFileOperations.Cobalt])]
[WopiAuthorize(Permission.Update, WopiResourceType.File)]
public async Task<IActionResult> ProcessCobalt(
string id,
[FromHeader(Name = WopiHeaders.CORRELATION_ID)] string? correlationId = null)
{
ArgumentNullException.ThrowIfNull(cobaltProcessor);
// Check permissions
if (!(await authorizationService.AuthorizeAsync(User, new FileResource(id), WopiOperations.Update)).Succeeded)
{
return Unauthorized();
}

var file = StorageProvider.GetWopiFile(id);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
//TODO: implement access_token_ttl https://learn.microsoft.com/openspecs/office_protocols/ms-wopi/adb48ba9-118a-43b6-82d7-9a508aad1583

if (Context.Request.Path.Value?.StartsWith("/wopi", StringComparison.OrdinalIgnoreCase) != true)
{
return AuthenticateResult.NoResult();
}

var token = Context.Request.Query[AccessTokenDefaults.ACCESS_TOKEN_QUERY_NAME].ToString();

if (Context.Request.Path.Value == "/wopibootstrapper")
Expand All @@ -43,6 +48,7 @@ protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
await securityHandler.GenerateAccessToken("Anonymous", Convert.ToBase64String(Encoding.UTF8.GetBytes(".\\"))));
}


if (!string.IsNullOrEmpty(token))
{
var principal = await securityHandler.GetPrincipal(token);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using WopiHost.Abstractions;

namespace WopiHost.Core.Security.Authorization;
Expand All @@ -10,18 +11,26 @@ namespace WopiHost.Core.Security.Authorization;
/// Creates an instance of <see cref="WopiAuthorizationHandler"/>.
/// </remarks>
/// <param name="securityHandler">AuthNZ handler.</param>
public class WopiAuthorizationHandler(IWopiSecurityHandler securityHandler)
: AuthorizationHandler<WopiAuthorizationRequirement, FileResource>
public class WopiAuthorizationHandler(IWopiSecurityHandler securityHandler)
: AuthorizationHandler<WopiAuthorizeAttribute, HttpContext>
{
/// <summary>
/// Performs resource-based authorization check.
/// </summary>
/// <param name="context">Context of the <see cref="AuthorizationHandler{TRequirement, TResource}"/></param>
/// <param name="requirement">Security requirement to be fulfilled (e.g. a permission).</param>
/// <param name="resource">Resource to check the security authorization requirement against.</param>
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, WopiAuthorizationRequirement requirement, FileResource resource)
/// <param name="requirement">Security requirement to be fulfilled (a permission on which resource).</param>
/// <param name="resource">httpContext resource</param>
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, WopiAuthorizeAttribute requirement, HttpContext resource)
{
if (await securityHandler.IsAuthorized(context.User, resource.FileId, requirement))
// try to retrieve resource identifier from the route
if (resource.Request.RouteValues.TryGetValue("id", out var fileIdRaw) &&
fileIdRaw is not null)
{
requirement.ResourceId = fileIdRaw.ToString();
}

// check if the user is authorized
if (await securityHandler.IsAuthorized(context.User, requirement))
{
context.Succeed(requirement);
}
Expand Down
42 changes: 42 additions & 0 deletions src/WopiHost.Core/Security/Authorization/WopiAuthorizeAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using Microsoft.AspNetCore.Authorization;
using WopiHost.Abstractions;

namespace WopiHost.Core.Security.Authorization;

/// <summary>
/// Represents an authorization requirement for a given combination of resource, user, and action.
/// </summary>
/// <remarks>
/// Creates an instance of <see cref="WopiAuthorizeAttribute"/> initialized with <paramref name="permission"/>.
/// </remarks>
/// <param name="permission">Permissions required for a given combination of resource, user, and action.</param>
/// <param name="resourceType">type of the resource.</param>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
public class WopiAuthorizeAttribute(
Permission permission,
WopiResourceType resourceType) : AuthorizeAttribute, IWopiAuthorizationRequirement, IAuthorizationRequirement, IAuthorizationRequirementData
{
/// <summary>
/// Gets a permissions required for a given combination of resource, user, and action.
/// </summary>
public Permission Permission { get; } = permission;

/// <summary>
/// Gets the type of the resource.
/// </summary>
public WopiResourceType ResourceType { get; } = resourceType;

/// <summary>
/// Gets or sets the identifier of the resource.
/// </summary>
public string? ResourceId { get; set; }

/// <summary>
/// Gets the requirements.
/// </summary>
/// <returns></returns>
public IEnumerable<IAuthorizationRequirement> GetRequirements()
{
yield return this;
}
}
16 changes: 0 additions & 16 deletions src/WopiHost.Core/Security/FileResource.cs

This file was deleted.

29 changes: 0 additions & 29 deletions src/WopiHost.Core/Security/WopiOperations.cs

This file was deleted.

Loading

0 comments on commit c792420

Please sign in to comment.