Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
@page "/authorized-test"
@using CodeBeam.UltimateAuth.Core.Defaults
@attribute [UAuthAuthorize]
@inherits UAuthFlowPageBase

Expand All @@ -19,6 +20,10 @@

<MudDivider Class="my-2" />

<UAuthStateView Roles="Admin" Permissions="@UAuthActions.Sessions.GetChainAdmin">
<MudText>This is admin view content.</MudText>
</UAuthStateView>

<MudText Typo="Typo.caption" Color="Color.Primary">
UltimateAuth protects this resource based on your session and permissions.
</MudText>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace CodeBeam.UltimateAuth.Core.Contracts;

public enum AuthorizationMatchMode
{
Any = 0,
All = 1,
Category = 2
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using CodeBeam.UltimateAuth.Core.Domain;
using CodeBeam.UltimateAuth.Core.Contracts;
using CodeBeam.UltimateAuth.Core.Domain;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Components;

Expand Down Expand Up @@ -40,11 +41,29 @@ public partial class UAuthStateView : UAuthReactiveComponentBase
public string? Policy { get; set; }

/// <summary>
/// Gets or sets a value indicating whether all set conditions must be matched for the operation to succeed.
/// Null parameters don't count as condition.
/// Determines how authorization conditions are evaluated.
///
/// <para>
/// <see cref="AuthorizationMatchMode.Any"/>:
/// Any configured condition may succeed.
/// </para>
///
/// <para>
/// <see cref="AuthorizationMatchMode.All"/>:
/// All configured conditions and values must succeed.
/// </para>
///
/// <para>
/// <see cref="AuthorizationMatchMode.Category"/>:
/// At least one value from each configured category must succeed.
/// For example:
/// one matching role AND one matching permission.
/// </para>
///
/// Null or empty parameters are ignored.
/// </summary>
[Parameter]
public bool MatchAll { get; set; } = true;
public AuthorizationMatchMode MatchMode { get; set; } = AuthorizationMatchMode.Category;

[Parameter]
public bool RequireActive { get; set; } = true;
Expand Down Expand Up @@ -92,34 +111,67 @@ private async Task<bool> EvaluateAuthorizationAsync()
if (!AuthState.IsAuthenticated)
return false;

var roles = _rolesParsed;
var permissions = _permissionsParsed;
var roleResults = _rolesParsed
.Select(AuthState.IsInRole)
.ToList();

var results = new List<bool>();
var permissionResults = _permissionsParsed
.Select(AuthState.HasPermission)
.ToList();

if (roles.Count > 0)
bool? policyResult = null;

if (!string.IsNullOrWhiteSpace(Policy))
{
results.Add(MatchAll
? roles.All(AuthState.IsInRole)
: roles.Any(AuthState.IsInRole));
policyResult = await EvaluatePolicyAsync();
}

if (permissions.Count > 0)
return MatchMode switch
{
results.Add(MatchAll
? permissions.All(AuthState.HasPermission)
: permissions.Any(AuthState.HasPermission));
}
AuthorizationMatchMode.Any
=> EvaluateAny(roleResults, permissionResults, policyResult),

if (!string.IsNullOrWhiteSpace(Policy))
results.Add(await EvaluatePolicyAsync());
AuthorizationMatchMode.All
=> EvaluateAll(roleResults, permissionResults, policyResult),

if (results.Count == 0)
return true;
AuthorizationMatchMode.Category
=> EvaluateCategory(roleResults, permissionResults, policyResult),

_ => false
};
}

private static bool EvaluateAny(IReadOnlyList<bool> roles, IReadOnlyList<bool> permissions, bool? policy)
{
return roles.Any(x => x) || permissions.Any(x => x) || policy == true;
}

private static bool EvaluateAll(IReadOnlyList<bool> roles, IReadOnlyList<bool> permissions, bool? policy)
{
if (roles.Count > 0 && roles.Any(x => !x))
return false;

if (permissions.Count > 0 && permissions.Any(x => !x))
return false;

if (policy.HasValue && !policy.Value)
return false;

return true;
}

private static bool EvaluateCategory(IReadOnlyList<bool> roles, IReadOnlyList<bool> permissions, bool? policy)
{
if (roles.Count > 0 && !roles.Any(x => x))
return false;

if (permissions.Count > 0 && !permissions.Any(x => x))
return false;

if (policy.HasValue && !policy.Value)
return false;

return MatchAll
? results.All(x => x)
: results.Any(x => x);
return true;
}

private void EvaluateSessionState()
Expand Down Expand Up @@ -171,6 +223,6 @@ private async Task<bool> EvaluatePolicyAsync()

private string BuildAuthKey()
{
return $"{Roles}|{Permissions}|{Policy}|{MatchAll}";
return $"{Roles}|{Permissions}|{Policy}|{MatchMode}";
}
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
using Bunit;
using CodeBeam.UltimateAuth.Authorization.Contracts;
using CodeBeam.UltimateAuth.Authorization.Reference;
using CodeBeam.UltimateAuth.Client;
using CodeBeam.UltimateAuth.Client.Blazor;
using CodeBeam.UltimateAuth.Core.Contracts;
using CodeBeam.UltimateAuth.Core.Domain;
using CodeBeam.UltimateAuth.Tests.Unit.Helpers;
using FluentAssertions;
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.DependencyInjection;
using Moq;
using System.Security.Claims;

namespace CodeBeam.UltimateAuth.Tests.Unit;

Expand Down Expand Up @@ -113,7 +112,7 @@ public void Should_Require_All_When_MatchAll_True()

var cut = RenderWithAuth(ctx, state, p => p
.Add(x => x.Roles, "admin,user")
.Add(x => x.MatchAll, true)
.Add(x => x.MatchMode, AuthorizationMatchMode.All)
.Add(x => x.NotAuthorized, Html("<div>no</div>"))
);

Expand All @@ -129,7 +128,47 @@ public void Should_Allow_Any_When_MatchAll_False()

var cut = RenderWithAuth(ctx, state, p => p
.Add(x => x.Roles, "admin,user")
.Add(x => x.MatchAll, false)
.Add(x => x.MatchMode, AuthorizationMatchMode.Any)
.Add(x => x.Authorized, s => b => b.AddContent(0, "ok"))
);

cut.Markup.Should().Contain("ok");
}

[Fact]
public void Should_Fail_When_One_Category_Does_Not_Match_In_Category_Mode()
{
using var ctx = new BunitContext();

var state = TestAuthState.WithRoles("admin");

ctx.Services.AddSingleton(Mock.Of<IAuthorizationService>());

var cut = RenderWithAuth(ctx, state, p => p
.Add(x => x.Roles, "admin,user")
.Add(x => x.Permissions, "write")
.Add(x => x.MatchMode, AuthorizationMatchMode.Category)
.Add(x => x.NotAuthorized, Html("<div>no</div>"))
);

cut.Markup.Should().Contain("no");
}

[Fact]
public void Should_Require_At_Least_One_Match_Per_Category_When_MatchMode_Is_Category()
{
using var ctx = new BunitContext();

var state = TestAuthState.Create(
roles: ["admin"],
permissions: ["write"]);

ctx.Services.AddSingleton(Mock.Of<IAuthorizationService>());

var cut = RenderWithAuth(ctx, state, p => p
.Add(x => x.Roles, "admin,user")
.Add(x => x.Permissions, "write,delete")
.Add(x => x.MatchMode, AuthorizationMatchMode.Category)
.Add(x => x.Authorized, s => b => b.AddContent(0, "ok"))
);

Expand Down
40 changes: 36 additions & 4 deletions tests/CodeBeam.UltimateAuth.Tests.Unit/Helpers/TestAuthState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,7 @@ public static UAuthState WithSession(SessionState sessionState)
return state;
}

public static UAuthState Full(
string userId,
string[] roles,
string[] permissions)
public static UAuthState Full(string userId, string[] roles, string[] permissions)
{
var claims = new List<(string, string)>();

Expand All @@ -80,4 +77,39 @@ public static UAuthState Full(

return Authenticated(userId, claims.ToArray());
}

public static UAuthState Create(string userId = "user-1", string[]? roles = null, string[]? permissions = null, SessionState sessionState = SessionState.Active, UserStatus userStatus = UserStatus.Active)
{
var claims = new List<(string Type, string Value)>();

if (roles is not null)
{
claims.AddRange(
roles.Select(x => (ClaimTypes.Role, x)));
}

if (permissions is not null)
{
claims.AddRange(
permissions.Select(x => ("uauth:permission", x)));
}

var state = Authenticated(userId, claims.ToArray());

var identity = state.Identity! with
{
SessionState = sessionState,
UserStatus = userStatus
};

var snapshot = new AuthStateSnapshot
{
Identity = identity,
Claims = state.Claims
};

state.ApplySnapshot(snapshot, DateTimeOffset.UtcNow);

return state;
}
}
Loading