Skip to content

Commit 790e1fa

Browse files
committed
Add privilege-aware password input component
1 parent d981a32 commit 790e1fa

File tree

13 files changed

+1581
-52
lines changed

13 files changed

+1581
-52
lines changed

Directory.Packages.props

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313
<PackageVersion Include="Equatable.Generator" Version="2.1.0" />
1414
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.1" />
1515
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="10.0.1" />
16-
<PackageVersion Include="Microsoft.AspNetCore.Components.Authorization" Version="10.0.0" />
17-
<PackageVersion Include="Microsoft.AspNetCore.Components.Web" Version="10.0.0" />
16+
<PackageVersion Include="Microsoft.AspNetCore.Components.Authorization" Version="10.0.1" />
17+
<PackageVersion Include="Microsoft.AspNetCore.Components.Web" Version="10.0.1" />
1818
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.1" />
1919
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="10.0.1" />
2020
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />

samples/Sample.Application/Sample.Application.Client/Layout/NavMenu.razor

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<div class="top-row ps-3 navbar navbar-dark">
1+
<div class="top-row ps-3 navbar navbar-dark">
22
<div class="container-fluid">
33
<a class="navbar-brand" href="">Sample.Application</a>
44
</div>
@@ -31,6 +31,11 @@
3131
<span class="bi bi-shield-lock-nav-menu" aria-hidden="true"></span> Privileges
3232
</NavLink>
3333
</div>
34+
<div class="nav-item px-3">
35+
<div class="nav-link">
36+
@RendererInfo.Name
37+
</div>
38+
</div>
3439
</nav>
3540
</div>
3641

samples/Sample.Application/Sample.Application.Client/Pages/PrivilegeDemo.razor

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,11 @@ else
7575
<label class="form-label">Internal Notes</label>
7676
<PrivilegeInputTextArea class="form-control" @bind-Value="Model.InternalNotes" Subject="Product" Field="InternalNotes" />
7777
</div>
78+
<div class="col-md-4">
79+
<label class="form-label">Password</label>
80+
<PrivilegeInputPassword class="form-control" @bind-Value="Model.Password" Subject="Product" Field="Password" Togglable="false" ToggleClass="text-body-tertiary" />
81+
</div>
82+
7883
</div>
7984

8085
<button type="submit" class="btn btn-primary mt-3">Save</button>
@@ -90,7 +95,7 @@ else
9095
</tr>
9196
</thead>
9297
<tbody>
93-
@foreach (var field in new[] { "Title", "Summary", "Cost", "InternalNotes" })
98+
@foreach (var field in new[] { "Title", "Summary", "Cost", "InternalNotes", "Password" })
9499
{
95100
<tr>
96101
<td>@field</td>
@@ -121,5 +126,7 @@ else
121126
public string Summary { get; set; } = "Summary goes here";
122127
public decimal Cost { get; set; } = 42.5m;
123128
public string InternalNotes { get; set; } = "Secret stuff";
129+
130+
public string Password { get; set; } = "P@ssw0rd!";
124131
}
125132
}

samples/Sample.Application/Sample.Application.Client/Services/PrivilegeContextProvider.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,20 +17,20 @@ public ValueTask<PrivilegeContext> GetContextAsync(ClaimsPrincipal? claimsPrinci
1717
// Simulate different privilege sets. This could be swapped based on a fake user id.
1818
var builder = new PrivilegeBuilder()
1919
// Aliases
20-
.Alias("Crud", new[] { "create", "read", "update", "delete" }, PrivilegeMatch.Action)
21-
.Alias("PublicFields", new[] { "Title", "Summary" }, PrivilegeMatch.Qualifier)
22-
.Alias("SensitiveFields", new[] { "Cost", "InternalNotes" }, PrivilegeMatch.Qualifier)
20+
.Alias("Crud", ["create", "read", "update", "delete"], PrivilegeMatch.Action)
21+
.Alias("PublicFields", ["Title", "Summary"], PrivilegeMatch.Qualifier)
22+
.Alias("SensitiveFields", ["Cost", "InternalNotes"], PrivilegeMatch.Qualifier)
2323

2424
// Global allowances
2525
.Allow("read", PrivilegeRule.Any) // read everything
26-
.Allow("update", "Product", new[] { "Title", "Summary" }) // update selected product fields
26+
.Allow("update", "Product", ["Title", "Summary", "Password"]) // update selected product fields
2727
.Allow("create", "Product")
2828
.Allow("read", "Order")
2929

3030
// Forbid some specifics
3131
.Forbid("delete", "Product")
32-
.Forbid("update", "Product", new[] { "Cost" }) // override broader rules
33-
.Forbid("read", "Product", new[] { "InternalNotes" });
32+
.Forbid("update", "Product", ["Cost"]) // override broader rules
33+
.Forbid("read", "Product", ["InternalNotes"]);
3434

3535
_cached = builder.Build();
3636
return ValueTask.FromResult(_cached);

samples/Sample.Application/Sample.Application/Components/Pages/PrivilegeDemo.razor

Whitespace-only changes.

samples/Sample.Application/Sample.Application/Components/Pages/Privileges.razor

Whitespace-only changes.

samples/Sample.Application/Sample.Application/Program.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using Sample.Application.Components;
77

88
namespace Sample.Application;
9+
910
public class Program
1011
{
1112
public static void Main(string[] args)

samples/Sample.Application/Sample.Application/SamplePrivilegeContextProvider.cs

Whitespace-only changes.

src/Privileged.Components/PrivilegeContextView.cs

Lines changed: 45 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ namespace Privileged.Components;
4141
/// &lt;/PrivilegeForm&gt;
4242
/// &lt;/Loaded&gt;
4343
/// &lt;/PrivilegeContextView&gt;
44-
///
44+
///
4545
/// &lt;!-- Simplified usage with ChildContent --&gt;
4646
/// &lt;PrivilegeContextView&gt;
4747
/// &lt;PrivilegeInputText @bind-Value="model.SecretField" Subject="Document" Field="Secret" /&gt;
@@ -53,37 +53,14 @@ namespace Privileged.Components;
5353
/// <seealso cref="AuthenticationStateProvider"/>
5454
public class PrivilegeContextView : ComponentBase
5555
{
56-
/// <summary>
57-
/// Gets or sets the provider for retrieving the privilege context.
58-
/// This service is automatically injected and must be registered in the dependency injection container.
59-
/// </summary>
60-
/// <value>
61-
/// An <see cref="IPrivilegeContextProvider"/> instance used to asynchronously load the privilege context.
62-
/// This service should be registered in the dependency injection container.
63-
/// </value>
64-
/// <remarks>
65-
/// The provider is responsible for loading user-specific privilege rules and aliases, typically from
66-
/// a database, cache, or external authorization service. The implementation should handle user identity
67-
/// and return appropriate privilege contexts based on the authenticated user's roles and permissions.
68-
/// </remarks>
69-
[Inject]
70-
protected IPrivilegeContextProvider PrivilegeContextProvider { get; set; } = default!;
56+
public PrivilegeContextView(
57+
IPrivilegeContextProvider privilegeContextProvider,
58+
AuthenticationStateProvider? authenticationStateProvider = null)
59+
{
60+
PrivilegeContextProvider = privilegeContextProvider;
61+
AuthenticationStateProvider = authenticationStateProvider;
62+
}
7163

72-
/// <summary>
73-
/// Gets or sets the authentication state provider used to retrieve the current user's authentication information.
74-
/// This service is automatically injected from the Blazor authentication system.
75-
/// </summary>
76-
/// <value>
77-
/// An <see cref="AuthenticationStateProvider"/> instance used to retrieve the current user's authentication state.
78-
/// This is passed to the privilege context provider to load user-specific privileges.
79-
/// </value>
80-
/// <remarks>
81-
/// The authentication state contains the current user's <see cref="System.Security.Claims.ClaimsPrincipal"/>,
82-
/// which is used by the privilege context provider to determine what privileges should be loaded.
83-
/// This enables user-specific privilege evaluation based on identity, roles, and claims.
84-
/// </remarks>
85-
[Inject]
86-
protected AuthenticationStateProvider AuthenticationStateProvider { get; set; } = default!;
8764

8865
/// <summary>
8966
/// Gets or sets the render fragment to display while the privilege context is loading.
@@ -164,6 +141,37 @@ public class PrivilegeContextView : ComponentBase
164141
[Parameter]
165142
public RenderFragment? ChildContent { get; set; }
166143

144+
145+
/// <summary>
146+
/// Gets or sets the provider for retrieving the privilege context.
147+
/// This service is automatically injected and must be registered in the dependency injection container.
148+
/// </summary>
149+
/// <value>
150+
/// An <see cref="IPrivilegeContextProvider"/> instance used to asynchronously load the privilege context.
151+
/// This service should be registered in the dependency injection container.
152+
/// </value>
153+
/// <remarks>
154+
/// The provider is responsible for loading user-specific privilege rules and aliases, typically from
155+
/// a database, cache, or external authorization service. The implementation should handle user identity
156+
/// and return appropriate privilege contexts based on the authenticated user's roles and permissions.
157+
/// </remarks>
158+
protected IPrivilegeContextProvider PrivilegeContextProvider { get; }
159+
160+
/// <summary>
161+
/// Gets or sets the authentication state provider used to retrieve the current user's authentication information.
162+
/// This service is automatically injected from the Blazor authentication system.
163+
/// </summary>
164+
/// <value>
165+
/// An <see cref="AuthenticationStateProvider"/> instance used to retrieve the current user's authentication state.
166+
/// This is passed to the privilege context provider to load user-specific privileges.
167+
/// </value>
168+
/// <remarks>
169+
/// The authentication state contains the current user's <see cref="System.Security.Claims.ClaimsPrincipal"/>,
170+
/// which is used by the privilege context provider to determine what privileges should be loaded.
171+
/// This enables user-specific privilege evaluation based on identity, roles, and claims.
172+
/// </remarks>
173+
protected AuthenticationStateProvider? AuthenticationStateProvider { get; }
174+
167175
/// <summary>
168176
/// Gets or sets the privilege context used for evaluating privilege rules.
169177
/// </summary>
@@ -177,7 +185,8 @@ public class PrivilegeContextView : ComponentBase
177185
/// privilege-aware behavior throughout the component tree. The loading state can be determined by
178186
/// checking if this property is <c>null</c>.
179187
/// </remarks>
180-
protected PrivilegeContext? PrivilegeContext { get; set; }
188+
protected PrivilegeContext? PrivilegeContext { get; private set; }
189+
181190

182191
/// <summary>
183192
/// Builds the render tree for the component, displaying the appropriate content based on the loading state
@@ -204,8 +213,8 @@ public class PrivilegeContextView : ComponentBase
204213
protected override void BuildRenderTree(RenderTreeBuilder builder)
205214
{
206215
builder.OpenComponent<CascadingValue<PrivilegeContext?>>(0);
207-
builder.AddAttribute(1, nameof(CascadingValue<PrivilegeContext?>.Value), PrivilegeContext);
208-
builder.AddAttribute(2, nameof(CascadingValue<PrivilegeContext?>.ChildContent), BuildChildContent());
216+
builder.AddAttribute(1, nameof(CascadingValue<>.Value), PrivilegeContext);
217+
builder.AddAttribute(2, nameof(CascadingValue<>.ChildContent), BuildChildContent());
209218
builder.CloseComponent();
210219
}
211220

@@ -232,8 +241,8 @@ protected override void BuildRenderTree(RenderTreeBuilder builder)
232241
/// </remarks>
233242
protected override async Task OnInitializedAsync()
234243
{
235-
var authenticationState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
236-
PrivilegeContext = await PrivilegeContextProvider.GetContextAsync(authenticationState.User);
244+
var authenticationState = AuthenticationStateProvider != null ? await AuthenticationStateProvider.GetAuthenticationStateAsync() : null;
245+
PrivilegeContext = await PrivilegeContextProvider.GetContextAsync(authenticationState?.User);
237246
StateHasChanged();
238247
}
239248

src/Privileged.Components/PrivilegeInputDate.cs

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@ namespace Privileged.Components;
2727
/// </remarks>
2828
/// <example>
2929
/// <code>
30-
/// &lt;PrivilegeInputDate @bind-Value="model.BirthDate"
31-
/// Subject="Employee"
32-
/// Field="BirthDate"
30+
/// &lt;PrivilegeInputDate @bind-Value="model.BirthDate"
31+
/// Subject="Employee"
32+
/// Field="BirthDate"
3333
/// type="date" /&gt;
3434
/// </code>
3535
/// </example>
@@ -64,7 +64,7 @@ public class PrivilegeInputDate<TValue> : InputDate<TValue>
6464
protected PrivilegeFormState? PrivilegeFormState { get; set; }
6565

6666
/// <summary>
67-
/// Gets or sets the subject for privilege evaluation.
67+
/// Gets or sets the subject for privilege evaluation.
6868
/// </summary>
6969
/// <value>
7070
/// The subject string used for privilege evaluation. If not specified, defaults to:
@@ -228,8 +228,8 @@ protected override void OnParametersSet()
228228
/// <item><description>Maintains the element reference for form integration</description></item>
229229
/// </list>
230230
/// <para>
231-
/// Note that when no read permission is granted, a single-line password input is rendered instead of
232-
/// a date input to maintain security and hide sensitive date information such as birth dates, hire dates,
231+
/// Note that when no read permission is granted, a single-line password input is rendered instead of
232+
/// a date input to maintain security and hide sensitive date information such as birth dates, hire dates,
233233
/// or other personally identifiable temporal data.
234234
/// </para>
235235
/// </remarks>
@@ -258,4 +258,20 @@ protected override void BuildRenderTree(Microsoft.AspNetCore.Components.Renderin
258258
builder.AddElementReferenceCapture(7, __inputReference => Element = __inputReference);
259259
builder.CloseElement();
260260
}
261+
262+
protected override string FormatValueAsString(TValue? value)
263+
{
264+
// treat MinValue as empty
265+
if (value is DateTime dateTime && dateTime == DateTime.MinValue)
266+
return string.Empty;
267+
268+
if (value is DateTimeOffset dateTimeOffset && dateTimeOffset == DateTimeOffset.MinValue)
269+
return string.Empty;
270+
271+
if (value is DateOnly dateOnly && dateOnly == DateOnly.MinValue)
272+
return string.Empty;
273+
274+
// otherwise, use base implementation
275+
return base.FormatValueAsString(value);
276+
}
261277
}

0 commit comments

Comments
 (0)