Skip to content
Draft
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
43 changes: 41 additions & 2 deletions src/Components/Authorization/src/AuthorizeRouteView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,14 @@ public AuthorizeRouteView()
[Parameter]
public RenderFragment<AuthenticationState>? NotAuthorized { get; set; }

/// <summary>
/// The page type that will be displayed if the user is not authorized.
/// The page type must implement <see cref="IComponent"/>.
/// </summary>
[Parameter]
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]
public Type? NotAuthorizedPage { get; set; }

/// <summary>
/// The content that will be displayed while asynchronous authorization is in progress.
/// </summary>
Expand Down Expand Up @@ -111,8 +119,39 @@ private void RenderContentInDefaultLayout(RenderTreeBuilder builder, RenderFragm

private void RenderNotAuthorizedInDefaultLayout(RenderTreeBuilder builder, AuthenticationState authenticationState)
{
var content = NotAuthorized ?? _defaultNotAuthorizedContent;
RenderContentInDefaultLayout(builder, content(authenticationState));
if (NotAuthorizedPage is not null)
{
if (!typeof(IComponent).IsAssignableFrom(NotAuthorizedPage))
{
throw new InvalidOperationException($"The type {NotAuthorizedPage.FullName} " +
$"does not implement {typeof(IComponent).FullName}.");
}

RenderPageInDefaultLayout(builder, NotAuthorizedPage);
}
else
{
var content = NotAuthorized ?? _defaultNotAuthorizedContent;
RenderContentInDefaultLayout(builder, content(authenticationState));
}
}

[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2111:RequiresUnreferencedCode",
Justification = "OpenComponent already has the right set of attributes")]
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2110:RequiresUnreferencedCode",
Justification = "OpenComponent already has the right set of attributes")]
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2118:RequiresUnreferencedCode",
Justification = "OpenComponent already has the right set of attributes")]
private void RenderPageInDefaultLayout(RenderTreeBuilder builder, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type pageType)
{
builder.OpenComponent<LayoutView>(0);
builder.AddComponentParameter(1, nameof(LayoutView.Layout), DefaultLayout);
builder.AddComponentParameter(2, nameof(LayoutView.ChildContent), (RenderFragment)(pageBuilder =>
{
pageBuilder.OpenComponent(0, pageType);
pageBuilder.CloseComponent();
}));
builder.CloseComponent();
}

private void RenderAuthorizingInDefaultLayout(RenderTreeBuilder builder)
Expand Down
2 changes: 2 additions & 0 deletions src/Components/Authorization/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
#nullable enable
Microsoft.AspNetCore.Components.Authorization.AuthorizeRouteView.NotAuthorizedPage.get -> System.Type?
Microsoft.AspNetCore.Components.Authorization.AuthorizeRouteView.NotAuthorizedPage.set -> void
97 changes: 97 additions & 0 deletions src/Components/Authorization/test/AuthorizeRouteViewTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,95 @@ public void UpdatesOutputWhenRouteDataChanges()
});
}

[Fact]
public void WhenNotAuthorized_RendersNotAuthorizedPageInsideLayout()
{
// Arrange
var routeData = new RouteData(typeof(TestPageRequiringAuthorization), EmptyParametersDictionary);
_testAuthorizationService.NextResult = AuthorizationResult.Failed();

// Act
_renderer.RenderRootComponent(_authorizeRouteViewComponentId, ParameterView.FromDictionary(new Dictionary<string, object>
{
{ nameof(AuthorizeRouteView.RouteData), routeData },
{ nameof(AuthorizeRouteView.DefaultLayout), typeof(TestLayout) },
{ nameof(AuthorizeRouteView.NotAuthorizedPage), typeof(TestNotAuthorizedPage) },
}));

// Assert: renders layout containing the NotAuthorizedPage component
var batch = _renderer.Batches.Single();
var layoutDiff = batch.GetComponentDiffs<TestLayout>().Single();
Assert.Collection(layoutDiff.Edits,
edit => AssertPrependText(batch, edit, "Layout starts here"),
edit =>
{
Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
AssertFrame.Component<TestNotAuthorizedPage>(batch.ReferenceFrames[edit.ReferenceFrameIndex]);
},
edit => AssertPrependText(batch, edit, "Layout ends here"));

// Assert: renders the not authorized page content
var pageDiff = batch.GetComponentDiffs<TestNotAuthorizedPage>().Single();
Assert.Collection(pageDiff.Edits,
edit => AssertPrependText(batch, edit, "This is the not authorized page"));
}

[Fact]
public void WhenNotAuthorized_NotAuthorizedPageTakesPriorityOverNotAuthorizedContent()
{
// Arrange: set both NotAuthorizedPage and NotAuthorized content
var routeData = new RouteData(typeof(TestPageRequiringAuthorization), EmptyParametersDictionary);
_testAuthorizationService.NextResult = AuthorizationResult.Failed();
_authenticationStateProvider.CurrentAuthStateTask = Task.FromResult(new AuthenticationState(
new ClaimsPrincipal(new TestIdentity { Name = "Bert" })));

RenderFragment<AuthenticationState> customNotAuthorized =
state => builder => builder.AddContent(0, $"Go away, {state.User.Identity.Name}");

// Act
_renderer.RenderRootComponent(_authorizeRouteViewComponentId, ParameterView.FromDictionary(new Dictionary<string, object>
{
{ nameof(AuthorizeRouteView.RouteData), routeData },
{ nameof(AuthorizeRouteView.DefaultLayout), typeof(TestLayout) },
{ nameof(AuthorizeRouteView.NotAuthorizedPage), typeof(TestNotAuthorizedPage) },
{ nameof(AuthorizeRouteView.NotAuthorized), customNotAuthorized },
}));

// Assert: renders layout containing the NotAuthorizedPage component (not the custom content)
var batch = _renderer.Batches.Single();
var layoutDiff = batch.GetComponentDiffs<TestLayout>().Single();
Assert.Collection(layoutDiff.Edits,
edit => AssertPrependText(batch, edit, "Layout starts here"),
edit =>
{
Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
AssertFrame.Component<TestNotAuthorizedPage>(batch.ReferenceFrames[edit.ReferenceFrameIndex]);
},
edit => AssertPrependText(batch, edit, "Layout ends here"));
}

[Fact]
public void WhenNotAuthorized_ThrowsForInvalidNotAuthorizedPageType()
{
// Arrange: set NotAuthorizedPage to a type that doesn't implement IComponent
var routeData = new RouteData(typeof(TestPageRequiringAuthorization), EmptyParametersDictionary);
_testAuthorizationService.NextResult = AuthorizationResult.Failed();

// Act & Assert
var exception = Assert.Throws<InvalidOperationException>(() =>
{
_renderer.RenderRootComponent(_authorizeRouteViewComponentId, ParameterView.FromDictionary(new Dictionary<string, object>
{
{ nameof(AuthorizeRouteView.RouteData), routeData },
{ nameof(AuthorizeRouteView.DefaultLayout), typeof(TestLayout) },
{ nameof(AuthorizeRouteView.NotAuthorizedPage), typeof(string) }, // string doesn't implement IComponent
}));
});

Assert.Contains("does not implement", exception.Message);
Assert.Contains("IComponent", exception.Message);
}

private static void AssertPrependText(CapturedBatch batch, RenderTreeEdit edit, string text)
{
Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
Expand All @@ -387,6 +476,14 @@ protected override void BuildRenderTree(RenderTreeBuilder builder)
}
}

class TestNotAuthorizedPage : ComponentBase
{
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
builder.AddContent(0, "This is the not authorized page");
}
}

class TestLayout : LayoutComponentBase
{
protected override void BuildRenderTree(RenderTreeBuilder builder)
Expand Down
19 changes: 19 additions & 0 deletions src/Components/test/E2ETest/Tests/AuthTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,16 @@ public void Router_RequireRole_NotAuthorized()
AssertExpectedLayoutUsed();
}

[Fact]
public void AuthorizeRouteView_NotAuthorizedPage_DisplaysPage()
{
SignInAs(null, null);
var appElement = MountAndNavigateToAuthTestWithNotAuthorizedPage(PageRequiringAuthorization);
Browser.Equal("You are not authorized to access this resource. This is the NotAuthorizedPage component.", () =>
appElement.FindElement(By.CssSelector("#not-authorized-page-content")).Text);
AssertExpectedLayoutUsed();
}

private void AssertExpectedLayoutUsed()
{
Browser.Exists(By.Id("auth-links"));
Expand All @@ -234,6 +244,15 @@ protected IWebElement MountAndNavigateToAuthTest(string authLinkText)
return appElement;
}

protected IWebElement MountAndNavigateToAuthTestWithNotAuthorizedPage(string authLinkText)
{
Navigate(ServerPathBase);
var appElement = Browser.MountTestComponent<BasicTestApp.AuthTest.AuthRouterWithNotAuthorizedPage>();
Browser.Exists(By.Id("auth-links"));
appElement.FindElement(By.LinkText(authLinkText)).Click();
return appElement;
}

private void SignInAs(string userName, string roles, bool useSeparateTab = false) =>
Browser.SignInAs(new Uri(_serverFixture.RootUri, "/subdir"), userName, roles, useSeparateTab);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
@page "/AuthHomeWithNotAuthorizedPage"

<p>This router uses NotAuthorizedPage for unauthorized access. Select an auth test below.</p>

<p id="auth-links">
<a href="PageRequiringAuthorization">Page requiring authorization</a>
</p>
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.Routing
@inject NavigationManager NavigationManager

@*
This router is independent of any other router that may exist within the same project.
It exists to test the AuthorizeRouteView.NotAuthorizedPage feature.
*@

<Router AppAssembly="@typeof(BasicTestApp.Program).Assembly">
<Found Context="routeData">
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(AuthRouterLayout)"
NotAuthorizedPage="@typeof(NotAuthorizedPage)">
<Authorizing>Authorizing...</Authorizing>
</AuthorizeRouteView>
</Found>
<NotFound>
<p>There's nothing here</p>
</NotFound>
</Router>

@code {
protected override void OnInitialized()
{
// Start at AuthHomeWithNotAuthorizedPage, not at any other component in the same app
var absoluteUriPath = new Uri(NavigationManager.Uri).GetLeftPart(UriPartial.Path);
var relativeUri = NavigationManager.ToBaseRelativePath(absoluteUriPath);
if (relativeUri == string.Empty)
{
NavigationManager.NavigateTo("AuthHomeWithNotAuthorizedPage");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
@page "/not-authorized-page"
<div id="not-authorized-page-content">
You are not authorized to access this resource. This is the NotAuthorizedPage component.
</div>
1 change: 1 addition & 0 deletions src/Components/test/testassets/BasicTestApp/Index.razor
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<option value="BasicTestApp.AfterRenderInteropComponent">After-render interop component</option>
<option value="BasicTestApp.AsyncEventHandlerComponent">Async event handlers</option>
<option value="BasicTestApp.AuthTest.AuthRouter">Auth cases</option>
<option value="BasicTestApp.AuthTest.AuthRouterWithNotAuthorizedPage">Auth cases with NotAuthorizedPage</option>
<option value="BasicTestApp.AuthTest.CascadingAuthenticationStateParent">Cascading authentication state</option>
<option value="BasicTestApp.BindCasesComponent">bind cases</option>
<option value="BasicTestApp.CascadingValueTest.CascadingValueSupplier">Cascading values</option>
Expand Down
Loading