Skip to content

Commit

Permalink
feat: Add minimum role authorize
Browse files Browse the repository at this point in the history
  • Loading branch information
data-miner00 committed Jan 23, 2024
1 parent 6e4f2e7 commit f3a3537
Show file tree
Hide file tree
Showing 3 changed files with 227 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
namespace Linker.WebApi.UnitTests.Filters;

using FluentAssertions;
using Linker.Core.Models;
using Linker.WebApi.Filters;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Filters;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;

internal sealed class MinimumRoleAuthorizeAttributeSteps
{
private MinimumRoleAuthorizeAttribute attribute;
private AuthorizationFilterContext filterContext;

private object? result;

public MinimumRoleAuthorizeAttributeSteps GivenCurrentUserHasRole(Role role)
{
var roleClaim = new Claim(ClaimTypes.Role, role.ToString());
var identity = new ClaimsIdentity([roleClaim]);
var user = new ClaimsPrincipal(identity);

var httpContext = new DefaultHttpContext
{
User = user,
};
var actionContext = new ActionContext(httpContext, new(), new());
this.filterContext = new AuthorizationFilterContext(actionContext, new List<IFilterMetadata>());

return this;
}

public MinimumRoleAuthorizeAttributeSteps GivenCurrentUserHasRole(string role)
{
var roleClaim = new Claim(ClaimTypes.Role, role);
var identity = new ClaimsIdentity([roleClaim]);
var user = new ClaimsPrincipal(identity);

var httpContext = new DefaultHttpContext
{
User = user,
};
var actionContext = new ActionContext(httpContext, new(), new());
this.filterContext = new AuthorizationFilterContext(actionContext, new List<IFilterMetadata>());

return this;
}

public MinimumRoleAuthorizeAttributeSteps GivenCurrentUserHasNoRole()
{
var identity = new ClaimsIdentity();
var user = new ClaimsPrincipal(identity);

var httpContext = new DefaultHttpContext
{
User = user,
};
var actionContext = new ActionContext(httpContext, new(), new());
this.filterContext = new AuthorizationFilterContext(actionContext, new List<IFilterMetadata>());

return this;
}

public MinimumRoleAuthorizeAttributeSteps GivenMinimumRoleRequiredIs(Role role)
{
this.attribute = new MinimumRoleAuthorizeAttribute(role);
return this;
}

public MinimumRoleAuthorizeAttributeSteps WhenIAuthorize()
{
this.attribute.OnAuthorization(this.filterContext);
this.result = this.filterContext.Result;
return this;
}

public MinimumRoleAuthorizeAttributeSteps ThenIExpectResultToBe(IActionResult result)
{
this.result.Should().BeAssignableTo<IActionResult>();
this.result.Should().Be(result);
return this;
}

public MinimumRoleAuthorizeAttributeSteps ThenIExpectToBeAuthorized()
{
this.result.Should().BeNull();
return this;
}

public MinimumRoleAuthorizeAttributeSteps ThenIExpectToBeUnauthorized()
{
this.result.Should().BeOfType<UnauthorizedResult>();
return this;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
namespace Linker.WebApi.UnitTests.Filters;

using Xunit;
using Linker.Core.Models;

public sealed class MinimumRoleAuthorizeAttributeTests
{
private readonly MinimumRoleAuthorizeAttributeSteps steps = new();

[Theory]
[InlineData(Role.Owner, Role.Owner)]
[InlineData(Role.Administrator, Role.Owner)]
[InlineData(Role.Administrator, Role.Administrator)]
[InlineData(Role.Moderator, Role.Owner)]
[InlineData(Role.Moderator, Role.Administrator)]
[InlineData(Role.Moderator, Role.Moderator)]
[InlineData(Role.User, Role.Owner)]
[InlineData(Role.User, Role.Administrator)]
[InlineData(Role.User, Role.Moderator)]
[InlineData(Role.User, Role.User)]
[InlineData(Role.ReadOnlyUser, Role.Owner)]
[InlineData(Role.ReadOnlyUser, Role.Administrator)]
[InlineData(Role.ReadOnlyUser, Role.Moderator)]
[InlineData(Role.ReadOnlyUser, Role.User)]
[InlineData(Role.ReadOnlyUser, Role.ReadOnlyUser)]
[InlineData(Role.Guest, Role.Owner)]
[InlineData(Role.Guest, Role.Administrator)]
[InlineData(Role.Guest, Role.Moderator)]
[InlineData(Role.Guest, Role.User)]
[InlineData(Role.Guest, Role.ReadOnlyUser)]
[InlineData(Role.Guest, Role.Guest)]
public void OnAuthorize_MetRequiredRoles_ReturnsSuccess(Role required, Role current)
{
this.steps
.GivenCurrentUserHasRole(current)
.GivenMinimumRoleRequiredIs(required)
.WhenIAuthorize()
.ThenIExpectToBeAuthorized();
}

[Theory]
[InlineData(Role.Owner, Role.Administrator)]
[InlineData(Role.Owner, Role.Moderator)]
[InlineData(Role.Owner, Role.User)]
[InlineData(Role.Owner, Role.ReadOnlyUser)]
[InlineData(Role.Owner, Role.Guest)]
[InlineData(Role.Administrator, Role.Moderator)]
[InlineData(Role.Administrator, Role.User)]
[InlineData(Role.Administrator, Role.ReadOnlyUser)]
[InlineData(Role.Administrator, Role.Guest)]
[InlineData(Role.Moderator, Role.ReadOnlyUser)]
[InlineData(Role.Moderator, Role.User)]
[InlineData(Role.Moderator, Role.Guest)]
[InlineData(Role.User, Role.ReadOnlyUser)]
[InlineData(Role.User, Role.Guest)]
[InlineData(Role.ReadOnlyUser, Role.Guest)]
public void OnAuthorize_FailedRequiredRoles_ReturnsUnauthorized(Role required, Role current)
{
this.steps
.GivenCurrentUserHasRole(current)
.GivenMinimumRoleRequiredIs(required)
.WhenIAuthorize()
.ThenIExpectToBeUnauthorized();
}

[Fact]
public void OnAuthorize_InvalidRoleProvided_ReturnsUnauthorized()
{
this.steps
.GivenCurrentUserHasRole("invalid role")
.GivenMinimumRoleRequiredIs(Role.Guest)
.WhenIAuthorize()
.ThenIExpectToBeUnauthorized();
}

[Fact]
public void OnAuthorize_NoRoleProvided_ReturnsUnauthorized()
{
this.steps
.GivenCurrentUserHasNoRole()
.GivenMinimumRoleRequiredIs(Role.Guest)
.WhenIAuthorize()
.ThenIExpectToBeUnauthorized();
}
}
39 changes: 39 additions & 0 deletions src/Linker.WebApi/Filters/MinimumRoleAuthorizeAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
namespace Linker.WebApi.Filters;

using Linker.Core.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using System.Security.Claims;

/// <summary>
/// Authorize the minimum allowed role.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)]
public sealed class MinimumRoleAuthorizeAttribute : Attribute, IAuthorizationFilter
{
private readonly Role minimumRole;

/// <summary>
/// Initializes a new instance of the <see cref="MinimumRoleAuthorizeAttribute"/> class.
/// </summary>
/// <param name="minimumRole">The minimum role required.</param>
public MinimumRoleAuthorizeAttribute(Role minimumRole)
{
this.minimumRole = minimumRole;
}

/// <inheritdoc/>
public void OnAuthorization(AuthorizationFilterContext context)
{
var stringRole = context.HttpContext.User.FindFirstValue(ClaimTypes.Role);

if (stringRole is not null
&& Enum.TryParse(typeof(Role), stringRole, out var currentRole)
&& (Role)currentRole <= this.minimumRole)
{
return;
}

context.Result = new UnauthorizedResult();
}
}

0 comments on commit f3a3537

Please sign in to comment.