Skip to content

Commit 84cdc07

Browse files
committed
Refactor privilege management for nullable subjects
1 parent 9423d8b commit 84cdc07

18 files changed

+1158
-181
lines changed

src/Privileged.Components/PrivilegeButton.cs

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,12 +66,13 @@ public class PrivilegeButton : ComponentBase
6666
/// current user's privileges to determine if the button should be enabled.
6767
/// </para>
6868
/// </remarks>
69-
[Parameter, EditorRequired]
70-
public required string Action { get; set; }
69+
[Parameter]
70+
public string? Action { get; set; }
7171

7272
/// <summary>
7373
/// Gets or sets the subject to authorize for this button.
7474
/// The subject typically represents a resource, entity type, or domain object.
75+
/// When null, empty, or whitespace, all privileges are assumed to be granted.
7576
/// </summary>
7677
/// <value>
7778
/// The subject name representing the resource or entity to check permissions for (e.g., "Post", "User", "Order").
@@ -82,9 +83,14 @@ public class PrivilegeButton : ComponentBase
8283
/// The subject represents the target of the action - what the user is trying to perform the action on.
8384
/// For instance, in a content management system, subjects might include "Post", "Page", "User", or "Comment".
8485
/// </para>
86+
/// <para>
87+
/// <strong>Special behavior:</strong> When the Subject parameter is <c>null</c>, empty, or contains only whitespace,
88+
/// the component assumes all privileges are granted and will enable the button regardless
89+
/// of the privilege context rules. This allows for fallback behavior when subject information is not available.
90+
/// </para>
8591
/// </remarks>
86-
[Parameter, EditorRequired]
87-
public required string Subject { get; set; }
92+
[Parameter]
93+
public string? Subject { get; set; }
8894

8995
/// <summary>
9096
/// Gets or sets an optional qualifier that provides additional scoping for the privilege evaluation.
@@ -293,7 +299,9 @@ protected override void OnParametersSet()
293299

294300
BusyTemplate ??= builder => builder.AddContent(0, BusyText);
295301

296-
HasPermission = PrivilegeContext.Allowed(Action, Subject, Qualifier);
302+
HasPermission = string.IsNullOrWhiteSpace(Action)
303+
|| string.IsNullOrWhiteSpace(Subject)
304+
|| PrivilegeContext.Allowed(Action, Subject, Qualifier);
297305
}
298306

299307
/// <summary>
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
using Microsoft.AspNetCore.Components;
2+
using Microsoft.AspNetCore.Components.Authorization;
3+
using Microsoft.AspNetCore.Components.Rendering;
4+
5+
namespace Privileged.Components;
6+
7+
/// <summary>
8+
/// A component that manages privilege context loading and provides cascading access to the privilege context.
9+
/// This component asynchronously loads the privilege context using an <see cref="IPrivilegeContextProvider"/>
10+
/// and displays appropriate content based on the loading state.
11+
/// </summary>
12+
/// <remarks>
13+
/// <para>
14+
/// The <see cref="PrivilegeContextView"/> component serves as a wrapper that handles the asynchronous
15+
/// loading of privilege contexts and provides different rendering templates based on the loading state.
16+
/// It automatically retrieves the authentication state and passes it to the privilege context provider.
17+
/// </para>
18+
/// <para>
19+
/// Content rendering precedence:
20+
/// <list type="number">
21+
/// <item>If the privilege context is <c>null</c> (loading): <see cref="Loading"/> template is rendered.</item>
22+
/// <item>If the privilege context is loaded and <see cref="Loaded"/> is specified: <see cref="Loaded"/> template is rendered.</item>
23+
/// <item>If the privilege context is loaded and <see cref="Loaded"/> is not specified: <see cref="ChildContent"/> is rendered.</item>
24+
/// </list>
25+
/// </para>
26+
/// <para>
27+
/// The component provides the loaded <see cref="PrivilegeContext"/> as a cascading value to all child components,
28+
/// enabling privilege-aware components throughout the component tree.
29+
/// </para>
30+
/// </remarks>
31+
/// <seealso cref="PrivilegeContext"/>
32+
/// <seealso cref="IPrivilegeContextProvider"/>
33+
/// <seealso cref="AuthenticationStateProvider"/>
34+
public class PrivilegeContextView : ComponentBase
35+
{
36+
/// <summary>
37+
/// Gets or sets the provider for retrieving the privilege context.
38+
/// </summary>
39+
/// <value>
40+
/// An <see cref="IPrivilegeContextProvider"/> instance used to asynchronously load the privilege context.
41+
/// This service should be registered in the dependency injection container.
42+
/// </value>
43+
[Inject]
44+
protected IPrivilegeContextProvider PrivilegeContextProvider { get; set; } = default!;
45+
46+
/// <summary>
47+
/// Gets or sets the authentication state provider.
48+
/// </summary>
49+
/// <value>
50+
/// An <see cref="AuthenticationStateProvider"/> instance used to retrieve the current user's authentication state.
51+
/// This is passed to the privilege context provider to load user-specific privileges.
52+
/// </value>
53+
[Inject]
54+
protected AuthenticationStateProvider AuthenticationStateProvider { get; set; } = default!;
55+
56+
/// <summary>
57+
/// Gets or sets the render fragment to display while the privilege context is loading.
58+
/// </summary>
59+
/// <value>
60+
/// A <see cref="RenderFragment"/> that defines the content to show during the loading state.
61+
/// If not specified, a default "Loading ..." message will be displayed.
62+
/// </value>
63+
[Parameter]
64+
public RenderFragment? Loading { get; set; } = builder => builder.AddMarkupContent(0, "Loading ...");
65+
66+
/// <summary>
67+
/// Gets or sets the render fragment to display when the privilege context is loaded.
68+
/// </summary>
69+
/// <value>
70+
/// A <see cref="RenderFragment"/> that defines the content to show when the privilege context
71+
/// has been successfully loaded. This takes precedence over <see cref="ChildContent"/> when specified.
72+
/// </value>
73+
[Parameter]
74+
public RenderFragment? Loaded { get; set; }
75+
76+
/// <summary>
77+
/// Gets or sets the child content to render within the privilege context.
78+
/// </summary>
79+
/// <value>
80+
/// A <see cref="RenderFragment"/> that defines the default content to render when the privilege context
81+
/// is loaded and <see cref="Loaded"/> is not specified.
82+
/// </value>
83+
[Parameter]
84+
public RenderFragment? ChildContent { get; set; }
85+
86+
/// <summary>
87+
/// Gets or sets the privilege context used for evaluating privilege rules.
88+
/// </summary>
89+
/// <value>
90+
/// The loaded <see cref="PrivilegeContext"/> instance, or <c>null</c> if the context is still loading.
91+
/// This value is set asynchronously during component initialization.
92+
/// </value>
93+
protected PrivilegeContext? PrivilegeContext { get; set; }
94+
95+
/// <summary>
96+
/// Builds the render tree for the component, displaying the appropriate content based on the loading state
97+
/// and wrapping the content in a cascading value that provides the privilege context to child components.
98+
/// </summary>
99+
/// <param name="builder">
100+
/// The <see cref="RenderTreeBuilder"/> used to construct the component's render tree.
101+
/// </param>
102+
/// <remarks>
103+
/// <para>
104+
/// The rendering logic follows this precedence:
105+
/// <list type="number">
106+
/// <item>Wraps all content in a <see cref="CascadingValue{TValue}"/> with the current <see cref="PrivilegeContext"/>.</item>
107+
/// <item>If <see cref="PrivilegeContext"/> is <c>null</c>: Renders <see cref="Loading"/> content.</item>
108+
/// <item>If <see cref="PrivilegeContext"/> is loaded and <see cref="Loaded"/> is specified: Renders <see cref="Loaded"/> content.</item>
109+
/// <item>If <see cref="PrivilegeContext"/> is loaded and <see cref="Loaded"/> is not specified: Renders <see cref="ChildContent"/>.</item>
110+
/// </list>
111+
/// </para>
112+
/// <para>
113+
/// The cascading value enables any child components that declare a <c>[CascadingParameter] PrivilegeContext</c>
114+
/// to automatically receive the loaded privilege context.
115+
/// </para>
116+
/// </remarks>
117+
protected override void BuildRenderTree(RenderTreeBuilder builder)
118+
{
119+
builder.OpenComponent<CascadingValue<PrivilegeContext?>>(0);
120+
builder.AddAttribute(1, nameof(CascadingValue<PrivilegeContext?>.Value), PrivilegeContext);
121+
builder.AddAttribute(2, nameof(CascadingValue<PrivilegeContext?>.ChildContent), BuildChildContent());
122+
builder.CloseComponent();
123+
}
124+
125+
/// <summary>
126+
/// Asynchronously initializes the privilege context by retrieving it from the provider.
127+
/// This method is called once when the component is first initialized.
128+
/// </summary>
129+
/// <returns>A task that represents the asynchronous initialization operation.</returns>
130+
protected override async Task OnInitializedAsync()
131+
{
132+
var authenticationState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
133+
PrivilegeContext = await PrivilegeContextProvider.GetContextAsync(authenticationState.User);
134+
StateHasChanged();
135+
}
136+
137+
/// <summary>
138+
/// Builds the child content render fragment for the cascading value based on the current loading state.
139+
/// </summary>
140+
/// <returns>
141+
/// A <see cref="RenderFragment"/> that renders the appropriate content based on whether the
142+
/// <see cref="PrivilegeContext"/> is loaded and which templates are available.
143+
/// </returns>
144+
private RenderFragment BuildChildContent()
145+
{
146+
return childBuilder =>
147+
{
148+
if (PrivilegeContext is null)
149+
{
150+
childBuilder.AddContent(0, Loading);
151+
}
152+
else if (Loaded is not null)
153+
{
154+
childBuilder.AddContent(1, Loaded);
155+
}
156+
else if (ChildContent is not null)
157+
{
158+
childBuilder.AddContent(2, ChildContent);
159+
}
160+
};
161+
}
162+
}

src/Privileged.Components/PrivilegeContextView.razor

Lines changed: 0 additions & 66 deletions
This file was deleted.

src/Privileged.Components/PrivilegeInputCheckbox.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ public class PrivilegeInputCheckbox : InputCheckbox
1616

1717
/// <summary>
1818
/// Gets or sets the subject for privilege evaluation. Defaults to the model's type name if not specified.
19+
/// When null, empty, or whitespace, all privileges are assumed to be granted.
1920
/// </summary>
2021
[Parameter]
2122
public string? Subject { get; set; }
@@ -64,8 +65,11 @@ protected override void OnParametersSet()
6465
var subject = Subject ?? EditContext?.Model.GetType().Name;
6566
var qualifier = Field ?? NameAttributeValue;
6667

67-
HasReadPermission = PrivilegeContext.Allowed(ReadAction, subject, qualifier);
68-
HasUpdatePermission = PrivilegeContext.Allowed(UpdateAction, subject, qualifier);
68+
HasReadPermission = string.IsNullOrWhiteSpace(Subject)
69+
|| PrivilegeContext.Allowed(ReadAction, subject, qualifier);
70+
71+
HasUpdatePermission = string.IsNullOrWhiteSpace(Subject)
72+
|| PrivilegeContext.Allowed(UpdateAction, subject, qualifier);
6973

7074
if (HasUpdatePermission)
7175
return;

src/Privileged.Components/PrivilegeInputNumber.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ public class PrivilegeInputNumber<TValue> : InputNumber<TValue>
1717

1818
/// <summary>
1919
/// Gets or sets the subject for privilege evaluation. Defaults to the model's type name if not specified.
20+
/// When null, empty, or whitespace, all privileges are assumed to be granted.
2021
/// </summary>
2122
[Parameter]
2223
public string? Subject { get; set; }
@@ -65,8 +66,11 @@ protected override void OnParametersSet()
6566
var subject = Subject ?? EditContext?.Model.GetType().Name;
6667
var qualifier = Field ?? NameAttributeValue;
6768

68-
HasReadPermission = PrivilegeContext.Allowed(ReadAction, subject, qualifier);
69-
HasUpdatePermission = PrivilegeContext.Allowed(UpdateAction, subject, qualifier);
69+
HasReadPermission = string.IsNullOrWhiteSpace(Subject)
70+
|| PrivilegeContext.Allowed(ReadAction, subject, qualifier);
71+
72+
HasUpdatePermission = string.IsNullOrWhiteSpace(Subject)
73+
|| PrivilegeContext.Allowed(UpdateAction, subject, qualifier);
7074

7175
if (HasUpdatePermission)
7276
return;

src/Privileged.Components/PrivilegeInputSelect.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ public class PrivilegeInputSelect<TValue> : InputSelect<TValue>
1717

1818
/// <summary>
1919
/// Gets or sets the subject for privilege evaluation. Defaults to the model's type name if not specified.
20+
/// When null, empty, or whitespace, all privileges are assumed to be granted.
2021
/// </summary>
2122
[Parameter]
2223
public string? Subject { get; set; }
@@ -65,8 +66,11 @@ protected override void OnParametersSet()
6566
var subject = Subject ?? EditContext?.Model.GetType().Name;
6667
var qualifier = Field ?? NameAttributeValue;
6768

68-
HasReadPermission = PrivilegeContext.Allowed(ReadAction, subject, qualifier);
69-
HasUpdatePermission = PrivilegeContext.Allowed(UpdateAction, subject, qualifier);
69+
HasReadPermission = string.IsNullOrWhiteSpace(Subject)
70+
|| PrivilegeContext.Allowed(ReadAction, subject, qualifier);
71+
72+
HasUpdatePermission = string.IsNullOrWhiteSpace(Subject)
73+
|| PrivilegeContext.Allowed(UpdateAction, subject, qualifier);
7074

7175
if (HasUpdatePermission)
7276
return;

src/Privileged.Components/PrivilegeInputText.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ public class PrivilegeInputText : InputText
1616

1717
/// <summary>
1818
/// Gets or sets the subject for privilege evaluation. Defaults to the model's type name if not specified.
19+
/// When null, empty, or whitespace, all privileges are assumed to be granted.
1920
/// </summary>
2021
[Parameter]
2122
public string? Subject { get; set; }
@@ -64,8 +65,11 @@ protected override void OnParametersSet()
6465
var subject = Subject ?? EditContext?.Model.GetType().Name;
6566
var qualifier = Field ?? NameAttributeValue;
6667

67-
HasReadPermission = PrivilegeContext.Allowed(ReadAction, subject, qualifier);
68-
HasUpdatePermission = PrivilegeContext.Allowed(UpdateAction, subject, qualifier);
68+
HasReadPermission = string.IsNullOrWhiteSpace(Subject)
69+
|| PrivilegeContext.Allowed(ReadAction, subject, qualifier);
70+
71+
HasUpdatePermission = string.IsNullOrWhiteSpace(Subject)
72+
|| PrivilegeContext.Allowed(UpdateAction, subject, qualifier);
6973

7074
if (HasUpdatePermission)
7175
return;

src/Privileged.Components/PrivilegeInputTextArea.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ public class PrivilegeInputTextArea : InputTextArea
1616

1717
/// <summary>
1818
/// Gets or sets the subject for privilege evaluation. Defaults to the model's type name if not specified.
19+
/// When null, empty, or whitespace, all privileges are assumed to be granted.
1920
/// </summary>
2021
[Parameter]
2122
public string? Subject { get; set; }
@@ -64,8 +65,11 @@ protected override void OnParametersSet()
6465
var subject = Subject ?? EditContext?.Model.GetType().Name;
6566
var qualifier = Field ?? NameAttributeValue;
6667

67-
HasReadPermission = PrivilegeContext.Allowed(ReadAction, subject, qualifier);
68-
HasUpdatePermission = PrivilegeContext.Allowed(UpdateAction, subject, qualifier);
68+
HasReadPermission = string.IsNullOrWhiteSpace(Subject)
69+
|| PrivilegeContext.Allowed(ReadAction, subject, qualifier);
70+
71+
HasUpdatePermission = string.IsNullOrWhiteSpace(Subject)
72+
|| PrivilegeContext.Allowed(UpdateAction, subject, qualifier);
6973

7074
if (HasUpdatePermission)
7175
return;

0 commit comments

Comments
 (0)