Skip to content

Commit 46cee74

Browse files
authored
UAuthStateView Component: Change MatchAll to MatchMode Parameter (#50)
1 parent 420b611 commit 46cee74

6 files changed

Lines changed: 168 additions & 32 deletions

File tree

samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Components/Pages/AuthorizedTestPage.razor

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
@page "/authorized-test"
2+
@using CodeBeam.UltimateAuth.Core.Defaults
23
@attribute [UAuthAuthorize]
34
@inherits UAuthFlowPageBase
45

@@ -19,6 +20,10 @@
1920

2021
<MudDivider Class="my-2" />
2122

23+
<UAuthStateView Roles="Admin" Permissions="@UAuthActions.Sessions.GetChainAdmin">
24+
<MudText>This is admin view content.</MudText>
25+
</UAuthStateView>
26+
2227
<MudText Typo="Typo.caption" Color="Color.Primary">
2328
UltimateAuth protects this resource based on your session and permissions.
2429
</MudText>

src/CodeBeam.UltimateAuth.Core/Contracts/Access/AccessScope.cs renamed to src/CodeBeam.UltimateAuth.Core/Contracts/Authorization/AccessScope.cs

File renamed without changes.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
namespace CodeBeam.UltimateAuth.Core.Contracts;
2+
3+
public enum AuthorizationMatchMode
4+
{
5+
Any = 0,
6+
All = 1,
7+
Category = 2
8+
}

src/client/CodeBeam.UltimateAuth.Client.Blazor/Components/UAuthStateView.razor.cs

Lines changed: 76 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using CodeBeam.UltimateAuth.Core.Domain;
1+
using CodeBeam.UltimateAuth.Core.Contracts;
2+
using CodeBeam.UltimateAuth.Core.Domain;
23
using Microsoft.AspNetCore.Authorization;
34
using Microsoft.AspNetCore.Components;
45

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

4243
/// <summary>
43-
/// Gets or sets a value indicating whether all set conditions must be matched for the operation to succeed.
44-
/// Null parameters don't count as condition.
44+
/// Determines how authorization conditions are evaluated.
45+
///
46+
/// <para>
47+
/// <see cref="AuthorizationMatchMode.Any"/>:
48+
/// Any configured condition may succeed.
49+
/// </para>
50+
///
51+
/// <para>
52+
/// <see cref="AuthorizationMatchMode.All"/>:
53+
/// All configured conditions and values must succeed.
54+
/// </para>
55+
///
56+
/// <para>
57+
/// <see cref="AuthorizationMatchMode.Category"/>:
58+
/// At least one value from each configured category must succeed.
59+
/// For example:
60+
/// one matching role AND one matching permission.
61+
/// </para>
62+
///
63+
/// Null or empty parameters are ignored.
4564
/// </summary>
4665
[Parameter]
47-
public bool MatchAll { get; set; } = true;
66+
public AuthorizationMatchMode MatchMode { get; set; } = AuthorizationMatchMode.Category;
4867

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

95-
var roles = _rolesParsed;
96-
var permissions = _permissionsParsed;
114+
var roleResults = _rolesParsed
115+
.Select(AuthState.IsInRole)
116+
.ToList();
97117

98-
var results = new List<bool>();
118+
var permissionResults = _permissionsParsed
119+
.Select(AuthState.HasPermission)
120+
.ToList();
99121

100-
if (roles.Count > 0)
122+
bool? policyResult = null;
123+
124+
if (!string.IsNullOrWhiteSpace(Policy))
101125
{
102-
results.Add(MatchAll
103-
? roles.All(AuthState.IsInRole)
104-
: roles.Any(AuthState.IsInRole));
126+
policyResult = await EvaluatePolicyAsync();
105127
}
106128

107-
if (permissions.Count > 0)
129+
return MatchMode switch
108130
{
109-
results.Add(MatchAll
110-
? permissions.All(AuthState.HasPermission)
111-
: permissions.Any(AuthState.HasPermission));
112-
}
131+
AuthorizationMatchMode.Any
132+
=> EvaluateAny(roleResults, permissionResults, policyResult),
113133

114-
if (!string.IsNullOrWhiteSpace(Policy))
115-
results.Add(await EvaluatePolicyAsync());
134+
AuthorizationMatchMode.All
135+
=> EvaluateAll(roleResults, permissionResults, policyResult),
116136

117-
if (results.Count == 0)
118-
return true;
137+
AuthorizationMatchMode.Category
138+
=> EvaluateCategory(roleResults, permissionResults, policyResult),
139+
140+
_ => false
141+
};
142+
}
143+
144+
private static bool EvaluateAny(IReadOnlyList<bool> roles, IReadOnlyList<bool> permissions, bool? policy)
145+
{
146+
return roles.Any(x => x) || permissions.Any(x => x) || policy == true;
147+
}
148+
149+
private static bool EvaluateAll(IReadOnlyList<bool> roles, IReadOnlyList<bool> permissions, bool? policy)
150+
{
151+
if (roles.Count > 0 && roles.Any(x => !x))
152+
return false;
153+
154+
if (permissions.Count > 0 && permissions.Any(x => !x))
155+
return false;
156+
157+
if (policy.HasValue && !policy.Value)
158+
return false;
159+
160+
return true;
161+
}
162+
163+
private static bool EvaluateCategory(IReadOnlyList<bool> roles, IReadOnlyList<bool> permissions, bool? policy)
164+
{
165+
if (roles.Count > 0 && !roles.Any(x => x))
166+
return false;
167+
168+
if (permissions.Count > 0 && !permissions.Any(x => x))
169+
return false;
170+
171+
if (policy.HasValue && !policy.Value)
172+
return false;
119173

120-
return MatchAll
121-
? results.All(x => x)
122-
: results.Any(x => x);
174+
return true;
123175
}
124176

125177
private void EvaluateSessionState()
@@ -171,6 +223,6 @@ private async Task<bool> EvaluatePolicyAsync()
171223

172224
private string BuildAuthKey()
173225
{
174-
return $"{Roles}|{Permissions}|{Policy}|{MatchAll}";
226+
return $"{Roles}|{Permissions}|{Policy}|{MatchMode}";
175227
}
176228
}

tests/CodeBeam.UltimateAuth.Tests.Unit/Bunit/UAuthStateViewTests.cs

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
using Bunit;
2-
using CodeBeam.UltimateAuth.Authorization.Contracts;
32
using CodeBeam.UltimateAuth.Authorization.Reference;
43
using CodeBeam.UltimateAuth.Client;
54
using CodeBeam.UltimateAuth.Client.Blazor;
5+
using CodeBeam.UltimateAuth.Core.Contracts;
66
using CodeBeam.UltimateAuth.Core.Domain;
77
using CodeBeam.UltimateAuth.Tests.Unit.Helpers;
88
using FluentAssertions;
99
using Microsoft.AspNetCore.Components;
1010
using Microsoft.Extensions.DependencyInjection;
1111
using Moq;
12-
using System.Security.Claims;
1312

1413
namespace CodeBeam.UltimateAuth.Tests.Unit;
1514

@@ -113,7 +112,7 @@ public void Should_Require_All_When_MatchAll_True()
113112

114113
var cut = RenderWithAuth(ctx, state, p => p
115114
.Add(x => x.Roles, "admin,user")
116-
.Add(x => x.MatchAll, true)
115+
.Add(x => x.MatchMode, AuthorizationMatchMode.All)
117116
.Add(x => x.NotAuthorized, Html("<div>no</div>"))
118117
);
119118

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

130129
var cut = RenderWithAuth(ctx, state, p => p
131130
.Add(x => x.Roles, "admin,user")
132-
.Add(x => x.MatchAll, false)
131+
.Add(x => x.MatchMode, AuthorizationMatchMode.Any)
132+
.Add(x => x.Authorized, s => b => b.AddContent(0, "ok"))
133+
);
134+
135+
cut.Markup.Should().Contain("ok");
136+
}
137+
138+
[Fact]
139+
public void Should_Fail_When_One_Category_Does_Not_Match_In_Category_Mode()
140+
{
141+
using var ctx = new BunitContext();
142+
143+
var state = TestAuthState.WithRoles("admin");
144+
145+
ctx.Services.AddSingleton(Mock.Of<IAuthorizationService>());
146+
147+
var cut = RenderWithAuth(ctx, state, p => p
148+
.Add(x => x.Roles, "admin,user")
149+
.Add(x => x.Permissions, "write")
150+
.Add(x => x.MatchMode, AuthorizationMatchMode.Category)
151+
.Add(x => x.NotAuthorized, Html("<div>no</div>"))
152+
);
153+
154+
cut.Markup.Should().Contain("no");
155+
}
156+
157+
[Fact]
158+
public void Should_Require_At_Least_One_Match_Per_Category_When_MatchMode_Is_Category()
159+
{
160+
using var ctx = new BunitContext();
161+
162+
var state = TestAuthState.Create(
163+
roles: ["admin"],
164+
permissions: ["write"]);
165+
166+
ctx.Services.AddSingleton(Mock.Of<IAuthorizationService>());
167+
168+
var cut = RenderWithAuth(ctx, state, p => p
169+
.Add(x => x.Roles, "admin,user")
170+
.Add(x => x.Permissions, "write,delete")
171+
.Add(x => x.MatchMode, AuthorizationMatchMode.Category)
133172
.Add(x => x.Authorized, s => b => b.AddContent(0, "ok"))
134173
);
135174

tests/CodeBeam.UltimateAuth.Tests.Unit/Helpers/TestAuthState.cs

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,7 @@ public static UAuthState WithSession(SessionState sessionState)
6868
return state;
6969
}
7070

71-
public static UAuthState Full(
72-
string userId,
73-
string[] roles,
74-
string[] permissions)
71+
public static UAuthState Full(string userId, string[] roles, string[] permissions)
7572
{
7673
var claims = new List<(string, string)>();
7774

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

8178
return Authenticated(userId, claims.ToArray());
8279
}
80+
81+
public static UAuthState Create(string userId = "user-1", string[]? roles = null, string[]? permissions = null, SessionState sessionState = SessionState.Active, UserStatus userStatus = UserStatus.Active)
82+
{
83+
var claims = new List<(string Type, string Value)>();
84+
85+
if (roles is not null)
86+
{
87+
claims.AddRange(
88+
roles.Select(x => (ClaimTypes.Role, x)));
89+
}
90+
91+
if (permissions is not null)
92+
{
93+
claims.AddRange(
94+
permissions.Select(x => ("uauth:permission", x)));
95+
}
96+
97+
var state = Authenticated(userId, claims.ToArray());
98+
99+
var identity = state.Identity! with
100+
{
101+
SessionState = sessionState,
102+
UserStatus = userStatus
103+
};
104+
105+
var snapshot = new AuthStateSnapshot
106+
{
107+
Identity = identity,
108+
Claims = state.Claims
109+
};
110+
111+
state.ApplySnapshot(snapshot, DateTimeOffset.UtcNow);
112+
113+
return state;
114+
}
83115
}

0 commit comments

Comments
 (0)