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
15 changes: 6 additions & 9 deletions src/Components/Authorization/src/AuthorizeViewCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ namespace Microsoft.AspNetCore.Components.Authorization
public abstract class AuthorizeViewCore : ComponentBase
{
private AuthenticationState currentAuthenticationState;
private bool isAuthorized;
private bool? isAuthorized;

/// <summary>
/// The content that will be displayed if the user is authorized.
Expand Down Expand Up @@ -54,11 +54,11 @@ protected override void BuildRenderTree(RenderTreeBuilder builder)
{
// We're using the same sequence number for each of the content items here
// so that we can update existing instances if they are the same shape
if (currentAuthenticationState == null)
if (isAuthorized == null)
{
builder.AddContent(0, Authorizing);
}
else if (isAuthorized)
else if (isAuthorized == true)
{
var authorized = Authorized ?? ChildContent;
builder.AddContent(0, authorized?.Invoke(currentAuthenticationState));
Expand All @@ -85,13 +85,10 @@ protected override async Task OnParametersSetAsync()
throw new InvalidOperationException($"Authorization requires a cascading parameter of type Task<{nameof(AuthenticationState)}>. Consider using {typeof(CascadingAuthenticationState).Name} to supply this.");
}

// First render in pending state
// If the task has already completed, this render will be skipped
currentAuthenticationState = null;
// Clear the previous result of authorization
// This will cause the Authorizing state to be displayed until the authorization has been completed
isAuthorized = null;

// Then render in completed state
// Importantly, we *don't* call StateHasChanged between the following async steps,
// otherwise we'd display an incorrect UI state while waiting for IsAuthorizedAsync
currentAuthenticationState = await AuthenticationState;
isAuthorized = await IsAuthorizedAsync(currentAuthenticationState.User);
}
Expand Down
62 changes: 62 additions & 0 deletions src/Components/Authorization/test/AuthorizeViewTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,68 @@ public void RendersNothingUntilAuthorizationCompleted()
});
}

[Fact]
public void RendersAuthorizingUntilAuthorizationCompletedAsync()
{
// Covers https://github.com/dotnet/aspnetcore/pull/31794
// Arrange
var @event = new ManualResetEventSlim();
var authorizationService = new TestAsyncAuthorizationService();
authorizationService.NextResult = AuthorizationResult.Success();
var renderer = CreateTestRenderer(authorizationService);
renderer.OnUpdateDisplayComplete = () => { @event.Set(); };
var rootComponent = WrapInAuthorizeView(
authorizing: builder => builder.AddContent(0, "Auth pending..."),
authorized: context => builder => builder.AddContent(0, $"Hello, {context.User.Identity.Name}!"));

var authTcs = new TaskCompletionSource<AuthenticationState>();
// Complete the authentication beforehand
authTcs.SetResult(CreateAuthenticationState("Monsieur").Result);

rootComponent.AuthenticationState = authTcs.Task;

// Act/Assert 1: Auth pending
renderer.AssignRootComponentId(rootComponent);
rootComponent.TriggerRender();
var batch1 = renderer.Batches.Single();
var authorizeViewComponentId = batch1.GetComponentFrames<AuthorizeView>().Single().ComponentId;
var diff1 = batch1.DiffsByComponentId[authorizeViewComponentId].Single();
Assert.Collection(diff1.Edits, edit =>
{
Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
AssertFrame.Text(
batch1.ReferenceFrames[edit.ReferenceFrameIndex],
"Auth pending...");
});

// Act/Assert 2: Auth process completes asynchronously
@event.Reset();

// Wait for authorization to complete asynchronously
@event.Wait(Timeout);

Assert.Equal(2, renderer.Batches.Count);
var batch2 = renderer.Batches[1];
var diff2 = batch2.DiffsByComponentId[authorizeViewComponentId].Single();
Assert.Collection(diff2.Edits, edit =>
{
Assert.Equal(RenderTreeEditType.UpdateText, edit.Type);
Assert.Equal(0, edit.SiblingIndex);
AssertFrame.Text(
batch2.ReferenceFrames[edit.ReferenceFrameIndex],
"Hello, Monsieur!");
});

// Assert: The IAuthorizationService was given expected criteria
Assert.Collection(authorizationService.AuthorizeCalls, call =>
{
Assert.Equal("Monsieur", call.user.Identity.Name);
Assert.Null(call.resource);
Assert.Collection(call.requirements,
req => Assert.IsType<DenyAnonymousAuthorizationRequirement>(req));
});
}

[Fact]
public void RendersAuthorizingUntilAuthorizationCompleted()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;

namespace Microsoft.AspNetCore.Components.Authorization
{
public class TestAsyncAuthorizationService : IAuthorizationService
{
public AuthorizationResult NextResult { get; set; }
= AuthorizationResult.Failed();

public List<(ClaimsPrincipal user, object resource, IEnumerable<IAuthorizationRequirement> requirements)> AuthorizeCalls { get; }
= new List<(ClaimsPrincipal user, object resource, IEnumerable<IAuthorizationRequirement> requirements)>();

public async Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object resource, IEnumerable<IAuthorizationRequirement> requirements)
{
AuthorizeCalls.Add((user, resource, requirements));

// Make Authorization run asynchronously
await Task.Yield();

// The TestAuthorizationService doesn't actually apply any authorization requirements
// It just returns the specified NextResult, since we're not trying to test the logic
// in DefaultAuthorizationService or similar here. So it's up to tests to set a desired
// NextResult and assert that the expected criteria were passed by inspecting AuthorizeCalls.
return NextResult;
}

public Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object resource, string policyName)
=> throw new NotImplementedException();

}
}