Integrate AuthorizeView with actual authorization (#10487)
This commit is contained in:
parent
405d8bbdc9
commit
d18a033b1e
|
|
@ -8,6 +8,8 @@ using Microsoft.AspNetCore.Blazor.Services;
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
using Microsoft.AspNetCore.Components.Routing;
|
using Microsoft.AspNetCore.Components.Routing;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.JSInterop;
|
using Microsoft.JSInterop;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Blazor.Hosting
|
namespace Microsoft.AspNetCore.Blazor.Hosting
|
||||||
|
|
@ -92,6 +94,7 @@ namespace Microsoft.AspNetCore.Blazor.Hosting
|
||||||
services.AddSingleton<IComponentContext, WebAssemblyComponentContext>();
|
services.AddSingleton<IComponentContext, WebAssemblyComponentContext>();
|
||||||
services.AddSingleton<IUriHelper>(WebAssemblyUriHelper.Instance);
|
services.AddSingleton<IUriHelper>(WebAssemblyUriHelper.Instance);
|
||||||
services.AddSingleton<INavigationInterception>(WebAssemblyNavigationInterception.Instance);
|
services.AddSingleton<INavigationInterception>(WebAssemblyNavigationInterception.Instance);
|
||||||
|
services.AddSingleton<ILoggerFactory, WebAssemblyLoggerFactory>();
|
||||||
services.AddSingleton<HttpClient>(s =>
|
services.AddSingleton<HttpClient>(s =>
|
||||||
{
|
{
|
||||||
// Creating the URI helper needs to wait until the JS Runtime is initialized, so defer it.
|
// Creating the URI helper needs to wait until the JS Runtime is initialized, so defer it.
|
||||||
|
|
@ -102,6 +105,10 @@ namespace Microsoft.AspNetCore.Blazor.Hosting
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Needed for authorization
|
||||||
|
services.AddOptions();
|
||||||
|
services.TryAdd(ServiceDescriptor.Singleton(typeof(ILogger<>), typeof(WebAssemblyConsoleLogger<>)));
|
||||||
|
|
||||||
foreach (var configureServicesAction in _configureServicesActions)
|
foreach (var configureServicesAction in _configureServicesActions)
|
||||||
{
|
{
|
||||||
configureServicesAction(_BrowserHostBuilderContext, services);
|
configureServicesAction(_BrowserHostBuilderContext, services);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
// 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 Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Blazor.Services
|
||||||
|
{
|
||||||
|
internal class WebAssemblyConsoleLogger<T> : ILogger<T>, ILogger
|
||||||
|
{
|
||||||
|
public IDisposable BeginScope<TState>(TState state)
|
||||||
|
{
|
||||||
|
return NoOpDisposable.Instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsEnabled(LogLevel logLevel)
|
||||||
|
{
|
||||||
|
return logLevel >= LogLevel.Warning;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
|
||||||
|
{
|
||||||
|
var formattedMessage = formatter(state, exception);
|
||||||
|
Console.WriteLine($"[{logLevel}] {formattedMessage}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private class NoOpDisposable : IDisposable
|
||||||
|
{
|
||||||
|
public static NoOpDisposable Instance = new NoOpDisposable();
|
||||||
|
|
||||||
|
public void Dispose() { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
// 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 Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Blazor.Services
|
||||||
|
{
|
||||||
|
internal class WebAssemblyLoggerFactory : ILoggerFactory
|
||||||
|
{
|
||||||
|
public void AddProvider(ILoggerProvider provider)
|
||||||
|
{
|
||||||
|
// No-op
|
||||||
|
}
|
||||||
|
|
||||||
|
public ILogger CreateLogger(string categoryName)
|
||||||
|
=> new WebAssemblyConsoleLogger<object>();
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
// No-op
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
|
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
|
||||||
<Compile Include="Microsoft.AspNetCore.Components.netstandard2.0.cs" />
|
<Compile Include="Microsoft.AspNetCore.Components.netstandard2.0.cs" />
|
||||||
|
<Reference Include="Microsoft.AspNetCore.Authorization" />
|
||||||
<Reference Include="Microsoft.JSInterop" />
|
<Reference Include="Microsoft.JSInterop" />
|
||||||
<Reference Include="System.ComponentModel.Annotations" />
|
<Reference Include="System.ComponentModel.Annotations" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,13 @@ namespace Microsoft.AspNetCore.Components
|
||||||
[Microsoft.AspNetCore.Components.ParameterAttribute]
|
[Microsoft.AspNetCore.Components.ParameterAttribute]
|
||||||
public Microsoft.AspNetCore.Components.RenderFragment<Microsoft.AspNetCore.Components.AuthenticationState> ChildContent { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } private set { throw null; } }
|
public Microsoft.AspNetCore.Components.RenderFragment<Microsoft.AspNetCore.Components.AuthenticationState> ChildContent { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } private set { throw null; } }
|
||||||
[Microsoft.AspNetCore.Components.ParameterAttribute]
|
[Microsoft.AspNetCore.Components.ParameterAttribute]
|
||||||
public Microsoft.AspNetCore.Components.RenderFragment NotAuthorized { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } private set { throw null; } }
|
public Microsoft.AspNetCore.Components.RenderFragment<Microsoft.AspNetCore.Components.AuthenticationState> NotAuthorized { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } private set { throw null; } }
|
||||||
|
[Microsoft.AspNetCore.Components.ParameterAttribute]
|
||||||
|
public string Policy { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } private set { throw null; } }
|
||||||
|
[Microsoft.AspNetCore.Components.ParameterAttribute]
|
||||||
|
public string Roles { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } private set { throw null; } }
|
||||||
|
[Microsoft.AspNetCore.Components.ParameterAttribute]
|
||||||
|
public object Resource { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } private set { throw null; } }
|
||||||
protected override void BuildRenderTree(Microsoft.AspNetCore.Components.RenderTree.RenderTreeBuilder builder) { }
|
protected override void BuildRenderTree(Microsoft.AspNetCore.Components.RenderTree.RenderTreeBuilder builder) { }
|
||||||
[System.Diagnostics.DebuggerStepThroughAttribute]
|
[System.Diagnostics.DebuggerStepThroughAttribute]
|
||||||
protected override System.Threading.Tasks.Task OnParametersSetAsync() { throw null; }
|
protected override System.Threading.Tasks.Task OnParametersSetAsync() { throw null; }
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
// 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 Microsoft.AspNetCore.Authorization;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Components
|
||||||
|
{
|
||||||
|
// This is so the AuthorizeView can avoid implementing IAuthorizeData (even privately)
|
||||||
|
internal class AuthorizeDataAdapter : IAuthorizeData
|
||||||
|
{
|
||||||
|
private readonly AuthorizeView _component;
|
||||||
|
|
||||||
|
public AuthorizeDataAdapter(AuthorizeView component)
|
||||||
|
{
|
||||||
|
_component = component ?? throw new ArgumentNullException(nameof(component));
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Policy
|
||||||
|
{
|
||||||
|
get => _component.Policy;
|
||||||
|
set => throw new NotSupportedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Roles
|
||||||
|
{
|
||||||
|
get => _component.Roles;
|
||||||
|
set => throw new NotSupportedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthorizeView doesn't expose any such parameter, as it wouldn't be used anyway,
|
||||||
|
// since we already have the ClaimsPrincipal by the time AuthorizeView gets involved.
|
||||||
|
public string AuthenticationSchemes
|
||||||
|
{
|
||||||
|
get => null;
|
||||||
|
set => throw new NotSupportedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,20 +1,26 @@
|
||||||
@namespace Microsoft.AspNetCore.Components
|
@namespace Microsoft.AspNetCore.Components
|
||||||
|
@using System.Security.Claims
|
||||||
|
@using Microsoft.AspNetCore.Authorization
|
||||||
|
@inject IAuthorizationService AuthorizationService
|
||||||
|
@inject IAuthorizationPolicyProvider AuthorizationPolicyProvider
|
||||||
|
|
||||||
@if (currentAuthenticationState == null)
|
@if (currentAuthenticationState == null)
|
||||||
{
|
{
|
||||||
@Authorizing
|
@Authorizing
|
||||||
}
|
}
|
||||||
else if (IsAuthorized())
|
else if (isAuthorized)
|
||||||
{
|
{
|
||||||
@((Authorized ?? ChildContent)?.Invoke(currentAuthenticationState))
|
@((Authorized ?? ChildContent)?.Invoke(currentAuthenticationState))
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@NotAuthorized
|
@(NotAuthorized?.Invoke(currentAuthenticationState))
|
||||||
}
|
}
|
||||||
|
|
||||||
@functions {
|
@functions {
|
||||||
|
private IAuthorizeData[] selfAsAuthorizeData;
|
||||||
private AuthenticationState currentAuthenticationState;
|
private AuthenticationState currentAuthenticationState;
|
||||||
|
private bool isAuthorized;
|
||||||
|
|
||||||
[CascadingParameter] private Task<AuthenticationState> AuthenticationState { get; set; }
|
[CascadingParameter] private Task<AuthenticationState> AuthenticationState { get; set; }
|
||||||
|
|
||||||
|
|
@ -26,7 +32,7 @@ else
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The content that will be displayed if the user is not authorized.
|
/// The content that will be displayed if the user is not authorized.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Parameter] public RenderFragment NotAuthorized { get; private set; }
|
[Parameter] public RenderFragment<AuthenticationState> NotAuthorized { get; private set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The content that will be displayed if the user is authorized.
|
/// The content that will be displayed if the user is authorized.
|
||||||
|
|
@ -39,6 +45,29 @@ else
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Parameter] public RenderFragment Authorizing { get; private set; }
|
[Parameter] public RenderFragment Authorizing { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The policy name that determines whether the content can be displayed.
|
||||||
|
/// </summary>
|
||||||
|
[Parameter] public string Policy { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A comma delimited list of roles that are allowed to display the content.
|
||||||
|
/// </summary>
|
||||||
|
[Parameter] public string Roles { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The resource to which access is being controlled.
|
||||||
|
/// </summary>
|
||||||
|
[Parameter] public object Resource { get; private set; }
|
||||||
|
|
||||||
|
protected override void OnInit()
|
||||||
|
{
|
||||||
|
selfAsAuthorizeData = new[]
|
||||||
|
{
|
||||||
|
new AuthorizeDataAdapter((AuthorizeView)(object)this)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
protected override async Task OnParametersSetAsync()
|
protected override async Task OnParametersSetAsync()
|
||||||
{
|
{
|
||||||
// We allow 'ChildContent' for convenience in basic cases, and 'Authorized' for symmetry
|
// We allow 'ChildContent' for convenience in basic cases, and 'Authorized' for symmetry
|
||||||
|
|
@ -54,15 +83,17 @@ else
|
||||||
currentAuthenticationState = null;
|
currentAuthenticationState = null;
|
||||||
|
|
||||||
// Then render in completed state
|
// 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;
|
currentAuthenticationState = await AuthenticationState;
|
||||||
|
isAuthorized = await IsAuthorizedAsync(currentAuthenticationState.User);
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool IsAuthorized()
|
private async Task<bool> IsAuthorizedAsync(ClaimsPrincipal user)
|
||||||
{
|
{
|
||||||
// TODO: Support various authorization condition parameters, equivalent to those offered
|
var policy = await AuthorizationPolicy.CombineAsync(
|
||||||
// by the [Authorize] attribute, e.g., "Roles" and "Policy". This is on hold until we're
|
AuthorizationPolicyProvider, selfAsAuthorizeData);
|
||||||
// able to reference the policy evaluator APIs from this package.
|
var result = await AuthorizationService.AuthorizeAsync(user, Resource, policy);
|
||||||
|
return result.Succeeded;
|
||||||
return currentAuthenticationState.User?.Identity?.IsAuthenticated == true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<Reference Include="Microsoft.AspNetCore.Authorization" />
|
||||||
<Reference Include="Microsoft.JSInterop" />
|
<Reference Include="Microsoft.JSInterop" />
|
||||||
<Reference Include="System.ComponentModel.Annotations" />
|
<Reference Include="System.ComponentModel.Annotations" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,18 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using System.Security.Principal;
|
using System.Security.Principal;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Authorization.Infrastructure;
|
||||||
using Microsoft.AspNetCore.Components.RenderTree;
|
using Microsoft.AspNetCore.Components.RenderTree;
|
||||||
using Microsoft.AspNetCore.Components.Test.Helpers;
|
using Microsoft.AspNetCore.Components.Test.Helpers;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Components
|
namespace Microsoft.AspNetCore.Components
|
||||||
|
|
@ -24,7 +28,8 @@ namespace Microsoft.AspNetCore.Components
|
||||||
public void RendersNothingIfNotAuthorized()
|
public void RendersNothingIfNotAuthorized()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var renderer = new TestRenderer();
|
var authorizationService = new TestAuthorizationService();
|
||||||
|
var renderer = CreateTestRenderer(authorizationService);
|
||||||
var rootComponent = WrapInAuthorizeView(
|
var rootComponent = WrapInAuthorizeView(
|
||||||
childContent:
|
childContent:
|
||||||
context => builder => builder.AddContent(0, "This should not be rendered"));
|
context => builder => builder.AddContent(0, "This should not be rendered"));
|
||||||
|
|
@ -36,18 +41,27 @@ namespace Microsoft.AspNetCore.Components
|
||||||
// Assert
|
// Assert
|
||||||
var diff = renderer.Batches.Single().GetComponentDiffs<AuthorizeView>().Single();
|
var diff = renderer.Batches.Single().GetComponentDiffs<AuthorizeView>().Single();
|
||||||
Assert.Empty(diff.Edits);
|
Assert.Empty(diff.Edits);
|
||||||
|
|
||||||
|
// Assert: The IAuthorizationService was given expected criteria
|
||||||
|
Assert.Collection(authorizationService.AuthorizeCalls, call =>
|
||||||
|
{
|
||||||
|
Assert.Null(call.user.Identity);
|
||||||
|
Assert.Null(call.resource);
|
||||||
|
Assert.Collection(call.requirements,
|
||||||
|
req => Assert.IsType<DenyAnonymousAuthorizationRequirement>(req));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void RendersNotAuthorizedContentIfNotAuthorized()
|
public void RendersNotAuthorizedContentIfNotAuthorized()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var renderer = new TestRenderer();
|
var authorizationService = new TestAuthorizationService();
|
||||||
|
var renderer = CreateTestRenderer(authorizationService);
|
||||||
var rootComponent = WrapInAuthorizeView(
|
var rootComponent = WrapInAuthorizeView(
|
||||||
childContent:
|
|
||||||
context => builder => builder.AddContent(0, "This should not be rendered"),
|
|
||||||
notAuthorizedContent:
|
notAuthorizedContent:
|
||||||
builder => builder.AddContent(0, "You are not authorized"));
|
context => builder => builder.AddContent(0, $"You are not authorized, even though we know you are {context.User.Identity.Name}"));
|
||||||
|
rootComponent.AuthenticationState = CreateAuthenticationState("Nellie");
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
renderer.AssignRootComponentId(rootComponent);
|
renderer.AssignRootComponentId(rootComponent);
|
||||||
|
|
@ -60,7 +74,16 @@ namespace Microsoft.AspNetCore.Components
|
||||||
Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
|
Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
|
||||||
AssertFrame.Text(
|
AssertFrame.Text(
|
||||||
renderer.Batches.Single().ReferenceFrames[edit.ReferenceFrameIndex],
|
renderer.Batches.Single().ReferenceFrames[edit.ReferenceFrameIndex],
|
||||||
"You are not authorized");
|
"You are not authorized, even though we know you are Nellie");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert: The IAuthorizationService was given expected criteria
|
||||||
|
Assert.Collection(authorizationService.AuthorizeCalls, call =>
|
||||||
|
{
|
||||||
|
Assert.Equal("Nellie", call.user.Identity.Name);
|
||||||
|
Assert.Null(call.resource);
|
||||||
|
Assert.Collection(call.requirements,
|
||||||
|
req => Assert.IsType<DenyAnonymousAuthorizationRequirement>(req));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -68,7 +91,9 @@ namespace Microsoft.AspNetCore.Components
|
||||||
public void RendersNothingIfAuthorizedButNoChildContentOrAuthorizedContentProvided()
|
public void RendersNothingIfAuthorizedButNoChildContentOrAuthorizedContentProvided()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var renderer = new TestRenderer();
|
var authorizationService = new TestAuthorizationService();
|
||||||
|
authorizationService.NextResult = AuthorizationResult.Success();
|
||||||
|
var renderer = CreateTestRenderer(authorizationService);
|
||||||
var rootComponent = WrapInAuthorizeView();
|
var rootComponent = WrapInAuthorizeView();
|
||||||
rootComponent.AuthenticationState = CreateAuthenticationState("Nellie");
|
rootComponent.AuthenticationState = CreateAuthenticationState("Nellie");
|
||||||
|
|
||||||
|
|
@ -79,13 +104,24 @@ namespace Microsoft.AspNetCore.Components
|
||||||
// Assert
|
// Assert
|
||||||
var diff = renderer.Batches.Single().GetComponentDiffs<AuthorizeView>().Single();
|
var diff = renderer.Batches.Single().GetComponentDiffs<AuthorizeView>().Single();
|
||||||
Assert.Empty(diff.Edits);
|
Assert.Empty(diff.Edits);
|
||||||
|
|
||||||
|
// Assert: The IAuthorizationService was given expected criteria
|
||||||
|
Assert.Collection(authorizationService.AuthorizeCalls, call =>
|
||||||
|
{
|
||||||
|
Assert.Equal("Nellie", call.user.Identity.Name);
|
||||||
|
Assert.Null(call.resource);
|
||||||
|
Assert.Collection(call.requirements,
|
||||||
|
req => Assert.IsType<DenyAnonymousAuthorizationRequirement>(req));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void RendersChildContentIfAuthorized()
|
public void RendersChildContentIfAuthorized()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var renderer = new TestRenderer();
|
var authorizationService = new TestAuthorizationService();
|
||||||
|
authorizationService.NextResult = AuthorizationResult.Success();
|
||||||
|
var renderer = CreateTestRenderer(authorizationService);
|
||||||
var rootComponent = WrapInAuthorizeView(
|
var rootComponent = WrapInAuthorizeView(
|
||||||
childContent: context => builder =>
|
childContent: context => builder =>
|
||||||
builder.AddContent(0, $"You are authenticated as {context.User.Identity.Name}"));
|
builder.AddContent(0, $"You are authenticated as {context.User.Identity.Name}"));
|
||||||
|
|
@ -104,13 +140,24 @@ namespace Microsoft.AspNetCore.Components
|
||||||
renderer.Batches.Single().ReferenceFrames[edit.ReferenceFrameIndex],
|
renderer.Batches.Single().ReferenceFrames[edit.ReferenceFrameIndex],
|
||||||
"You are authenticated as Nellie");
|
"You are authenticated as Nellie");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Assert: The IAuthorizationService was given expected criteria
|
||||||
|
Assert.Collection(authorizationService.AuthorizeCalls, call =>
|
||||||
|
{
|
||||||
|
Assert.Equal("Nellie", call.user.Identity.Name);
|
||||||
|
Assert.Null(call.resource);
|
||||||
|
Assert.Collection(call.requirements,
|
||||||
|
req => Assert.IsType<DenyAnonymousAuthorizationRequirement>(req));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void RendersAuthorizedContentIfAuthorized()
|
public void RendersAuthorizedContentIfAuthorized()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var renderer = new TestRenderer();
|
var authorizationService = new TestAuthorizationService();
|
||||||
|
authorizationService.NextResult = AuthorizationResult.Success();
|
||||||
|
var renderer = CreateTestRenderer(authorizationService);
|
||||||
var rootComponent = WrapInAuthorizeView(
|
var rootComponent = WrapInAuthorizeView(
|
||||||
authorizedContent: context => builder =>
|
authorizedContent: context => builder =>
|
||||||
builder.AddContent(0, $"You are authenticated as {context.User.Identity.Name}"));
|
builder.AddContent(0, $"You are authenticated as {context.User.Identity.Name}"));
|
||||||
|
|
@ -129,13 +176,24 @@ namespace Microsoft.AspNetCore.Components
|
||||||
renderer.Batches.Single().ReferenceFrames[edit.ReferenceFrameIndex],
|
renderer.Batches.Single().ReferenceFrames[edit.ReferenceFrameIndex],
|
||||||
"You are authenticated as Nellie");
|
"You are authenticated as Nellie");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Assert: The IAuthorizationService was given expected criteria
|
||||||
|
Assert.Collection(authorizationService.AuthorizeCalls, call =>
|
||||||
|
{
|
||||||
|
Assert.Equal("Nellie", call.user.Identity.Name);
|
||||||
|
Assert.Null(call.resource);
|
||||||
|
Assert.Collection(call.requirements,
|
||||||
|
req => Assert.IsType<DenyAnonymousAuthorizationRequirement>(req));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void RespondsToChangeInAuthorizationState()
|
public void RespondsToChangeInAuthorizationState()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var renderer = new TestRenderer();
|
var authorizationService = new TestAuthorizationService();
|
||||||
|
authorizationService.NextResult = AuthorizationResult.Success();
|
||||||
|
var renderer = CreateTestRenderer(authorizationService);
|
||||||
var rootComponent = WrapInAuthorizeView(
|
var rootComponent = WrapInAuthorizeView(
|
||||||
childContent: context => builder =>
|
childContent: context => builder =>
|
||||||
builder.AddContent(0, $"You are authenticated as {context.User.Identity.Name}"));
|
builder.AddContent(0, $"You are authenticated as {context.User.Identity.Name}"));
|
||||||
|
|
@ -147,6 +205,7 @@ namespace Microsoft.AspNetCore.Components
|
||||||
rootComponent.TriggerRender();
|
rootComponent.TriggerRender();
|
||||||
var authorizeViewComponentId = renderer.Batches.Single()
|
var authorizeViewComponentId = renderer.Batches.Single()
|
||||||
.GetComponentFrames<AuthorizeView>().Single().ComponentId;
|
.GetComponentFrames<AuthorizeView>().Single().ComponentId;
|
||||||
|
authorizationService.AuthorizeCalls.Clear();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
rootComponent.AuthenticationState = CreateAuthenticationState("Ronaldo");
|
rootComponent.AuthenticationState = CreateAuthenticationState("Ronaldo");
|
||||||
|
|
@ -164,13 +223,23 @@ namespace Microsoft.AspNetCore.Components
|
||||||
batch.ReferenceFrames[edit.ReferenceFrameIndex],
|
batch.ReferenceFrames[edit.ReferenceFrameIndex],
|
||||||
"You are authenticated as Ronaldo");
|
"You are authenticated as Ronaldo");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Assert: The IAuthorizationService was given expected criteria
|
||||||
|
Assert.Collection(authorizationService.AuthorizeCalls, call =>
|
||||||
|
{
|
||||||
|
Assert.Equal("Ronaldo", call.user.Identity.Name);
|
||||||
|
Assert.Null(call.resource);
|
||||||
|
Assert.Collection(call.requirements,
|
||||||
|
req => Assert.IsType<DenyAnonymousAuthorizationRequirement>(req));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ThrowsIfBothChildContentAndAuthorizedContentProvided()
|
public void ThrowsIfBothChildContentAndAuthorizedContentProvided()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var renderer = new TestRenderer();
|
var authorizationService = new TestAuthorizationService();
|
||||||
|
var renderer = CreateTestRenderer(authorizationService);
|
||||||
var rootComponent = WrapInAuthorizeView(
|
var rootComponent = WrapInAuthorizeView(
|
||||||
authorizedContent: context => builder => { },
|
authorizedContent: context => builder => { },
|
||||||
childContent: context => builder => { });
|
childContent: context => builder => { });
|
||||||
|
|
@ -187,12 +256,12 @@ namespace Microsoft.AspNetCore.Components
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var @event = new ManualResetEventSlim();
|
var @event = new ManualResetEventSlim();
|
||||||
var renderer = new TestRenderer()
|
var authorizationService = new TestAuthorizationService();
|
||||||
{
|
var renderer = CreateTestRenderer(authorizationService);
|
||||||
OnUpdateDisplayComplete = () => { @event.Set(); },
|
renderer.OnUpdateDisplayComplete = () => { @event.Set(); };
|
||||||
};
|
|
||||||
var rootComponent = WrapInAuthorizeView(
|
var rootComponent = WrapInAuthorizeView(
|
||||||
notAuthorizedContent: builder => builder.AddContent(0, "You are not authorized"));
|
notAuthorizedContent:
|
||||||
|
context => builder => builder.AddContent(0, "You are not authorized"));
|
||||||
var authTcs = new TaskCompletionSource<AuthenticationState>();
|
var authTcs = new TaskCompletionSource<AuthenticationState>();
|
||||||
rootComponent.AuthenticationState = authTcs.Task;
|
rootComponent.AuthenticationState = authTcs.Task;
|
||||||
|
|
||||||
|
|
@ -228,10 +297,10 @@ namespace Microsoft.AspNetCore.Components
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var @event = new ManualResetEventSlim();
|
var @event = new ManualResetEventSlim();
|
||||||
var renderer = new TestRenderer()
|
var authorizationService = new TestAuthorizationService();
|
||||||
{
|
authorizationService.NextResult = AuthorizationResult.Success();
|
||||||
OnUpdateDisplayComplete = () => { @event.Set(); },
|
var renderer = CreateTestRenderer(authorizationService);
|
||||||
};
|
renderer.OnUpdateDisplayComplete = () => { @event.Set(); };
|
||||||
var rootComponent = WrapInAuthorizeView(
|
var rootComponent = WrapInAuthorizeView(
|
||||||
authorizingContent: builder => builder.AddContent(0, "Auth pending..."),
|
authorizingContent: builder => builder.AddContent(0, "Auth pending..."),
|
||||||
authorizedContent: context => builder => builder.AddContent(0, $"Hello, {context.User.Identity.Name}!"));
|
authorizedContent: context => builder => builder.AddContent(0, $"Hello, {context.User.Identity.Name}!"));
|
||||||
|
|
@ -276,13 +345,96 @@ namespace Microsoft.AspNetCore.Components
|
||||||
batch2.ReferenceFrames[edit.ReferenceFrameIndex],
|
batch2.ReferenceFrames[edit.ReferenceFrameIndex],
|
||||||
"Hello, Monsieur!");
|
"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 IncludesPolicyInAuthorizeCall()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var authorizationService = new TestAuthorizationService();
|
||||||
|
var renderer = CreateTestRenderer(authorizationService);
|
||||||
|
var rootComponent = WrapInAuthorizeView(policy: "MyTestPolicy");
|
||||||
|
rootComponent.AuthenticationState = CreateAuthenticationState("Nellie");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
renderer.AssignRootComponentId(rootComponent);
|
||||||
|
rootComponent.TriggerRender();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Collection(authorizationService.AuthorizeCalls, call =>
|
||||||
|
{
|
||||||
|
Assert.Equal("Nellie", call.user.Identity.Name);
|
||||||
|
Assert.Null(call.resource);
|
||||||
|
Assert.Collection(call.requirements,
|
||||||
|
req => Assert.Equal("MyTestPolicy", ((TestPolicyRequirement)req).PolicyName));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IncludesRolesInAuthorizeCall()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var authorizationService = new TestAuthorizationService();
|
||||||
|
var renderer = CreateTestRenderer(authorizationService);
|
||||||
|
var rootComponent = WrapInAuthorizeView(roles: "SuperTestRole1, SuperTestRole2");
|
||||||
|
rootComponent.AuthenticationState = CreateAuthenticationState("Nellie");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
renderer.AssignRootComponentId(rootComponent);
|
||||||
|
rootComponent.TriggerRender();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Collection(authorizationService.AuthorizeCalls, call =>
|
||||||
|
{
|
||||||
|
Assert.Equal("Nellie", call.user.Identity.Name);
|
||||||
|
Assert.Null(call.resource);
|
||||||
|
Assert.Collection(call.requirements, req => Assert.Equal(
|
||||||
|
new[] { "SuperTestRole1", "SuperTestRole2" },
|
||||||
|
((RolesAuthorizationRequirement)req).AllowedRoles));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IncludesResourceInAuthorizeCall()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var authorizationService = new TestAuthorizationService();
|
||||||
|
var renderer = CreateTestRenderer(authorizationService);
|
||||||
|
var resource = new object();
|
||||||
|
var rootComponent = WrapInAuthorizeView(resource: resource);
|
||||||
|
rootComponent.AuthenticationState = CreateAuthenticationState("Nellie");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
renderer.AssignRootComponentId(rootComponent);
|
||||||
|
rootComponent.TriggerRender();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Collection(authorizationService.AuthorizeCalls, call =>
|
||||||
|
{
|
||||||
|
Assert.Equal("Nellie", call.user.Identity.Name);
|
||||||
|
Assert.Same(resource, call.resource);
|
||||||
|
Assert.Collection(call.requirements, req =>
|
||||||
|
Assert.IsType<DenyAnonymousAuthorizationRequirement>(req));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private static TestAuthStateProviderComponent WrapInAuthorizeView(
|
private static TestAuthStateProviderComponent WrapInAuthorizeView(
|
||||||
RenderFragment<AuthenticationState> childContent = null,
|
RenderFragment<AuthenticationState> childContent = null,
|
||||||
RenderFragment<AuthenticationState> authorizedContent = null,
|
RenderFragment<AuthenticationState> authorizedContent = null,
|
||||||
RenderFragment notAuthorizedContent = null,
|
RenderFragment<AuthenticationState> notAuthorizedContent = null,
|
||||||
RenderFragment authorizingContent = null)
|
RenderFragment authorizingContent = null,
|
||||||
|
string policy = null,
|
||||||
|
string roles = null,
|
||||||
|
object resource = null)
|
||||||
{
|
{
|
||||||
return new TestAuthStateProviderComponent(builder =>
|
return new TestAuthStateProviderComponent(builder =>
|
||||||
{
|
{
|
||||||
|
|
@ -291,6 +443,9 @@ namespace Microsoft.AspNetCore.Components
|
||||||
builder.AddAttribute(2, nameof(AuthorizeView.Authorized), authorizedContent);
|
builder.AddAttribute(2, nameof(AuthorizeView.Authorized), authorizedContent);
|
||||||
builder.AddAttribute(3, nameof(AuthorizeView.NotAuthorized), notAuthorizedContent);
|
builder.AddAttribute(3, nameof(AuthorizeView.NotAuthorized), notAuthorizedContent);
|
||||||
builder.AddAttribute(4, nameof(AuthorizeView.Authorizing), authorizingContent);
|
builder.AddAttribute(4, nameof(AuthorizeView.Authorizing), authorizingContent);
|
||||||
|
builder.AddAttribute(5, nameof(AuthorizeView.Policy), policy);
|
||||||
|
builder.AddAttribute(6, nameof(AuthorizeView.Roles), roles);
|
||||||
|
builder.AddAttribute(7, nameof(AuthorizeView.Resource), resource);
|
||||||
builder.CloseComponent();
|
builder.CloseComponent();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -311,11 +466,31 @@ namespace Microsoft.AspNetCore.Components
|
||||||
{
|
{
|
||||||
builder.OpenComponent<CascadingValue<Task<AuthenticationState>>>(0);
|
builder.OpenComponent<CascadingValue<Task<AuthenticationState>>>(0);
|
||||||
builder.AddAttribute(1, nameof(CascadingValue<Task<AuthenticationState>>.Value), AuthenticationState);
|
builder.AddAttribute(1, nameof(CascadingValue<Task<AuthenticationState>>.Value), AuthenticationState);
|
||||||
builder.AddAttribute(2, RenderTreeBuilder.ChildContent, _childContent);
|
builder.AddAttribute(2, RenderTreeBuilder.ChildContent, (RenderFragment)(builder =>
|
||||||
|
{
|
||||||
|
builder.OpenComponent<NeverReRenderComponent>(0);
|
||||||
|
builder.AddAttribute(1, RenderTreeBuilder.ChildContent, _childContent);
|
||||||
|
builder.CloseComponent();
|
||||||
|
}));
|
||||||
builder.CloseComponent();
|
builder.CloseComponent();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This is useful to show that the reason why a CascadingValue refreshes is because the
|
||||||
|
// value itself changed, not just that we're re-rendering the entire tree and have to
|
||||||
|
// recurse into all descendants because we're passing ChildContent
|
||||||
|
class NeverReRenderComponent : ComponentBase
|
||||||
|
{
|
||||||
|
[Parameter] RenderFragment ChildContent { get; set; }
|
||||||
|
|
||||||
|
protected override bool ShouldRender() => false;
|
||||||
|
|
||||||
|
protected override void BuildRenderTree(RenderTreeBuilder builder)
|
||||||
|
{
|
||||||
|
builder.AddContent(0, ChildContent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static Task<AuthenticationState> CreateAuthenticationState(string username)
|
public static Task<AuthenticationState> CreateAuthenticationState(string username)
|
||||||
=> Task.FromResult(new AuthenticationState(
|
=> Task.FromResult(new AuthenticationState(
|
||||||
new ClaimsPrincipal(new TestIdentity { Name = username })));
|
new ClaimsPrincipal(new TestIdentity { Name = username })));
|
||||||
|
|
@ -328,5 +503,59 @@ namespace Microsoft.AspNetCore.Components
|
||||||
|
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public TestRenderer CreateTestRenderer(IAuthorizationService authorizationService)
|
||||||
|
{
|
||||||
|
var serviceCollection = new ServiceCollection();
|
||||||
|
serviceCollection.AddSingleton(authorizationService);
|
||||||
|
serviceCollection.AddSingleton<IAuthorizationPolicyProvider>(new TestAuthorizationPolicyProvider());
|
||||||
|
return new TestRenderer(serviceCollection.BuildServiceProvider());
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestAuthorizationService : 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 Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object resource, IEnumerable<IAuthorizationRequirement> requirements)
|
||||||
|
{
|
||||||
|
AuthorizeCalls.Add((user, resource, requirements));
|
||||||
|
|
||||||
|
// 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 Task.FromResult(NextResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object resource, string policyName)
|
||||||
|
=> throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestAuthorizationPolicyProvider : IAuthorizationPolicyProvider
|
||||||
|
{
|
||||||
|
private readonly AuthorizationOptions options = new AuthorizationOptions();
|
||||||
|
|
||||||
|
public Task<AuthorizationPolicy> GetDefaultPolicyAsync()
|
||||||
|
=> Task.FromResult(options.DefaultPolicy);
|
||||||
|
|
||||||
|
public Task<AuthorizationPolicy> GetFallbackPolicyAsync()
|
||||||
|
=> Task.FromResult(options.FallbackPolicy);
|
||||||
|
|
||||||
|
public Task<AuthorizationPolicy> GetPolicyAsync(string policyName) => Task.FromResult(
|
||||||
|
new AuthorizationPolicy(new[]
|
||||||
|
{
|
||||||
|
new TestPolicyRequirement { PolicyName = policyName }
|
||||||
|
},
|
||||||
|
new[] { $"TestScheme:{policyName}" }));
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TestPolicyRequirement : IAuthorizationRequirement
|
||||||
|
{
|
||||||
|
public string PolicyName { get; set; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,7 @@ namespace Microsoft.AspNetCore.Components.Test.Helpers
|
||||||
{
|
{
|
||||||
if (!ShouldHandleExceptions)
|
if (!ShouldHandleExceptions)
|
||||||
{
|
{
|
||||||
throw exception;
|
ExceptionDispatchInfo.Capture(exception).Throw();
|
||||||
}
|
}
|
||||||
|
|
||||||
HandledExceptions.Add(exception);
|
HandledExceptions.Add(exception);
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
||||||
[Fact]
|
[Fact]
|
||||||
public void CascadingAuthenticationState_Unauthenticated()
|
public void CascadingAuthenticationState_Unauthenticated()
|
||||||
{
|
{
|
||||||
SignInAs(null);
|
SignInAs(null, null);
|
||||||
|
|
||||||
var appElement = MountAndNavigateToAuthTest(CascadingAuthenticationStateLink);
|
var appElement = MountAndNavigateToAuthTest(CascadingAuthenticationStateLink);
|
||||||
|
|
||||||
|
|
@ -44,7 +44,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
||||||
[Fact]
|
[Fact]
|
||||||
public void CascadingAuthenticationState_Authenticated()
|
public void CascadingAuthenticationState_Authenticated()
|
||||||
{
|
{
|
||||||
SignInAs("someone cool");
|
SignInAs("someone cool", null);
|
||||||
|
|
||||||
var appElement = MountAndNavigateToAuthTest(CascadingAuthenticationStateLink);
|
var appElement = MountAndNavigateToAuthTest(CascadingAuthenticationStateLink);
|
||||||
|
|
||||||
|
|
@ -56,20 +56,58 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
||||||
[Fact]
|
[Fact]
|
||||||
public void AuthorizeViewCases_NoAuthorizationRule_Unauthenticated()
|
public void AuthorizeViewCases_NoAuthorizationRule_Unauthenticated()
|
||||||
{
|
{
|
||||||
SignInAs(null);
|
SignInAs(null, null);
|
||||||
MountAndNavigateToAuthTest(AuthorizeViewCases);
|
var appElement = MountAndNavigateToAuthTest(AuthorizeViewCases);
|
||||||
WaitUntilExists(By.CssSelector("#no-authorization-rule .not-authorized"));
|
WaitUntilExists(By.CssSelector("#no-authorization-rule .not-authorized"));
|
||||||
|
Browser.Equal("You're not authorized, anonymous", () =>
|
||||||
|
appElement.FindElement(By.CssSelector("#no-authorization-rule .not-authorized")).Text);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void AuthorizeViewCases_NoAuthorizationRule_Authenticated()
|
public void AuthorizeViewCases_NoAuthorizationRule_Authenticated()
|
||||||
{
|
{
|
||||||
SignInAs("Some User");
|
SignInAs("Some User", null);
|
||||||
var appElement = MountAndNavigateToAuthTest(AuthorizeViewCases);
|
var appElement = MountAndNavigateToAuthTest(AuthorizeViewCases);
|
||||||
Browser.Equal("Welcome, Some User!", () =>
|
Browser.Equal("Welcome, Some User!", () =>
|
||||||
appElement.FindElement(By.CssSelector("#no-authorization-rule .authorized")).Text);
|
appElement.FindElement(By.CssSelector("#no-authorization-rule .authorized")).Text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AuthorizeViewCases_RequireRole_Authenticated()
|
||||||
|
{
|
||||||
|
SignInAs("Some User", "IrrelevantRole,TestRole");
|
||||||
|
var appElement = MountAndNavigateToAuthTest(AuthorizeViewCases);
|
||||||
|
Browser.Equal("Welcome, Some User!", () =>
|
||||||
|
appElement.FindElement(By.CssSelector("#authorize-role .authorized")).Text);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AuthorizeViewCases_RequireRole_Unauthenticated()
|
||||||
|
{
|
||||||
|
SignInAs("Some User", "IrrelevantRole");
|
||||||
|
var appElement = MountAndNavigateToAuthTest(AuthorizeViewCases);
|
||||||
|
Browser.Equal("You're not authorized, Some User", () =>
|
||||||
|
appElement.FindElement(By.CssSelector("#authorize-role .not-authorized")).Text);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AuthorizeViewCases_RequirePolicy_Authenticated()
|
||||||
|
{
|
||||||
|
SignInAs("Bert", null);
|
||||||
|
var appElement = MountAndNavigateToAuthTest(AuthorizeViewCases);
|
||||||
|
Browser.Equal("Welcome, Bert!", () =>
|
||||||
|
appElement.FindElement(By.CssSelector("#authorize-policy .authorized")).Text);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AuthorizeViewCases_RequirePolicy_Unauthenticated()
|
||||||
|
{
|
||||||
|
SignInAs("Mallory", null);
|
||||||
|
var appElement = MountAndNavigateToAuthTest(AuthorizeViewCases);
|
||||||
|
Browser.Equal("You're not authorized, Mallory", () =>
|
||||||
|
appElement.FindElement(By.CssSelector("#authorize-policy .not-authorized")).Text);
|
||||||
|
}
|
||||||
|
|
||||||
IWebElement MountAndNavigateToAuthTest(string authLinkText)
|
IWebElement MountAndNavigateToAuthTest(string authLinkText)
|
||||||
{
|
{
|
||||||
Navigate(ServerPathBase);
|
Navigate(ServerPathBase);
|
||||||
|
|
@ -79,12 +117,12 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
||||||
return appElement;
|
return appElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
void SignInAs(string usernameOrNull)
|
void SignInAs(string usernameOrNull, string rolesOrNull)
|
||||||
{
|
{
|
||||||
const string authenticationPageUrl = "/Authentication";
|
const string authenticationPageUrl = "/Authentication";
|
||||||
var baseRelativeUri = usernameOrNull == null
|
var baseRelativeUri = usernameOrNull == null
|
||||||
? $"{authenticationPageUrl}?signout=true"
|
? $"{authenticationPageUrl}?signout=true"
|
||||||
: $"{authenticationPageUrl}?username={usernameOrNull}";
|
: $"{authenticationPageUrl}?username={usernameOrNull}&roles={rolesOrNull}";
|
||||||
Navigate(baseRelativeUri);
|
Navigate(baseRelativeUri);
|
||||||
WaitUntilExists(By.CssSelector("h1#authentication"));
|
WaitUntilExists(By.CssSelector("h1#authentication"));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,39 @@
|
||||||
<p class="authorized">Welcome, @context.User.Identity.Name!</p>
|
<p class="authorized">Welcome, @context.User.Identity.Name!</p>
|
||||||
</Authorized>
|
</Authorized>
|
||||||
<NotAuthorized>
|
<NotAuthorized>
|
||||||
<p class="not-authorized">You're not logged in.</p>
|
<p class="not-authorized">You're not authorized, @(context.User.Identity.Name ?? "anonymous")</p>
|
||||||
|
</NotAuthorized>
|
||||||
|
</AuthorizeView>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="authorize-role">
|
||||||
|
<h3>Scenario: Require role</h3>
|
||||||
|
|
||||||
|
<AuthorizeView Roles="TestRole">
|
||||||
|
<Authorizing>
|
||||||
|
<p class="authorizing">Authorizing...</p>
|
||||||
|
</Authorizing>
|
||||||
|
<Authorized>
|
||||||
|
<p class="authorized">Welcome, @context.User.Identity.Name!</p>
|
||||||
|
</Authorized>
|
||||||
|
<NotAuthorized>
|
||||||
|
<p class="not-authorized">You're not authorized, @(context.User.Identity.Name ?? "anonymous")</p>
|
||||||
|
</NotAuthorized>
|
||||||
|
</AuthorizeView>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="authorize-policy">
|
||||||
|
<h3>Scenario: Require policy</h3>
|
||||||
|
|
||||||
|
<AuthorizeView Policy="NameMustStartWithB">
|
||||||
|
<Authorizing>
|
||||||
|
<p class="authorizing">Authorizing...</p>
|
||||||
|
</Authorizing>
|
||||||
|
<Authorized>
|
||||||
|
<p class="authorized">Welcome, @context.User.Identity.Name!</p>
|
||||||
|
</Authorized>
|
||||||
|
<NotAuthorized>
|
||||||
|
<p class="not-authorized">You're not authorized, @(context.User.Identity.Name ?? "anonymous")</p>
|
||||||
</NotAuthorized>
|
</NotAuthorized>
|
||||||
</AuthorizeView>
|
</AuthorizeView>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,6 @@ namespace BasicTestApp.AuthTest
|
||||||
|
|
||||||
public string UserName { get; set; }
|
public string UserName { get; set; }
|
||||||
|
|
||||||
public Dictionary<string, string> ExposedClaims { get; set; }
|
public List<(string Type, string Value)> ExposedClaims { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ namespace BasicTestApp.AuthTest
|
||||||
if (data.IsAuthenticated)
|
if (data.IsAuthenticated)
|
||||||
{
|
{
|
||||||
var claims = new[] { new Claim(ClaimTypes.Name, data.UserName) }
|
var claims = new[] { new Claim(ClaimTypes.Name, data.UserName) }
|
||||||
.Concat(data.ExposedClaims.Select(c => new Claim(c.Key, c.Value)));
|
.Concat(data.ExposedClaims.Select(c => new Claim(c.Type, c.Value)));
|
||||||
identity = new ClaimsIdentity(claims, "Server authentication");
|
identity = new ClaimsIdentity(claims, "Server authentication");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,12 @@ namespace BasicTestApp
|
||||||
public void ConfigureServices(IServiceCollection services)
|
public void ConfigureServices(IServiceCollection services)
|
||||||
{
|
{
|
||||||
services.AddSingleton<AuthenticationStateProvider, ServerAuthenticationStateProvider>();
|
services.AddSingleton<AuthenticationStateProvider, ServerAuthenticationStateProvider>();
|
||||||
|
|
||||||
|
services.AddAuthorizationCore(options =>
|
||||||
|
{
|
||||||
|
options.AddPolicy("NameMustStartWithB", policy =>
|
||||||
|
policy.RequireAssertion(ctx => ctx.User.Identity.Name?.StartsWith("B") ?? false));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Configure(IComponentsApplicationBuilder app)
|
public void Configure(IComponentsApplicationBuilder app)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Security.Claims;
|
||||||
using BasicTestApp.AuthTest;
|
using BasicTestApp.AuthTest;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
|
@ -7,22 +9,28 @@ namespace Components.TestServer.Controllers
|
||||||
[Route("api/[controller]")]
|
[Route("api/[controller]")]
|
||||||
public class UserController : Controller
|
public class UserController : Controller
|
||||||
{
|
{
|
||||||
|
// Servers are not expected to expose everything from the server-side ClaimsPrincipal
|
||||||
|
// to the client. It's up to the developer to choose what kind of authentication state
|
||||||
|
// data is needed on the client so it can display suitable options in the UI.
|
||||||
|
// In this class, we inform the client only about certain roles and certain other claims.
|
||||||
|
static string[] ExposedRoles = new[] { "IrrelevantRole", "TestRole" };
|
||||||
|
|
||||||
// GET api/user
|
// GET api/user
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public ClientSideAuthenticationStateData Get()
|
public ClientSideAuthenticationStateData Get()
|
||||||
{
|
{
|
||||||
// Servers are not expected to expose everything from the server-side ClaimsPrincipal
|
|
||||||
// to the client. It's up to the developer to choose what kind of authentication state
|
|
||||||
// data is needed on the client so it can display suitable options in the UI.
|
|
||||||
|
|
||||||
return new ClientSideAuthenticationStateData
|
return new ClientSideAuthenticationStateData
|
||||||
{
|
{
|
||||||
IsAuthenticated = User.Identity.IsAuthenticated,
|
IsAuthenticated = User.Identity.IsAuthenticated,
|
||||||
UserName = User.Identity.Name,
|
UserName = User.Identity.Name,
|
||||||
ExposedClaims = User.Claims
|
ExposedClaims = User.Claims
|
||||||
.Where(c => c.Type == "test-claim")
|
.Where(c => c.Type == "test-claim" || IsExposedRole(c))
|
||||||
.ToDictionary(c => c.Type, c => c.Value)
|
.Select(c => (c.Type, c.Value)).ToList()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool IsExposedRole(Claim claim)
|
||||||
|
=> claim.Type == ClaimTypes.Role
|
||||||
|
&& ExposedRoles.Contains(claim.Value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,10 @@
|
||||||
User name:
|
User name:
|
||||||
<input name="username" />
|
<input name="username" />
|
||||||
</p>
|
</p>
|
||||||
|
<p>
|
||||||
|
Roles:
|
||||||
|
<input name="roles" />
|
||||||
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<button type="submit">Submit</button>
|
<button type="submit">Submit</button>
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -37,7 +41,11 @@
|
||||||
<p>
|
<p>
|
||||||
Authenticated: <strong>@User.Identity.IsAuthenticated</strong>
|
Authenticated: <strong>@User.Identity.IsAuthenticated</strong>
|
||||||
Username: <strong>@User.Identity.Name</strong>
|
Username: <strong>@User.Identity.Name</strong>
|
||||||
</p>
|
Roles:
|
||||||
|
<strong>
|
||||||
|
@string.Join(", ", User.Claims.Where(c => c.Type == ClaimTypes.Role).Select(c => c.Value).ToArray())
|
||||||
|
</strong>
|
||||||
|
</p>foreach
|
||||||
<a href="Authentication?signout=true">Sign out</a>
|
<a href="Authentication?signout=true">Sign out</a>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</body>
|
</body>
|
||||||
|
|
@ -61,6 +69,15 @@
|
||||||
new Claim("test-claim", "Test claim value"),
|
new Claim("test-claim", "Test claim value"),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var roles = Request.Query["roles"];
|
||||||
|
if (!string.IsNullOrEmpty(roles))
|
||||||
|
{
|
||||||
|
foreach (var role in roles.ToString().Split(','))
|
||||||
|
{
|
||||||
|
claims.Add(new Claim(ClaimTypes.Role, role));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await HttpContext.SignInAsync(
|
await HttpContext.SignInAsync(
|
||||||
new ClaimsPrincipal(new ClaimsIdentity(claims, "FakeAuthenticationType")));
|
new ClaimsPrincipal(new ClaimsIdentity(claims, "FakeAuthenticationType")));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,12 @@ namespace TestServer
|
||||||
});
|
});
|
||||||
services.AddServerSideBlazor();
|
services.AddServerSideBlazor();
|
||||||
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie();
|
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie();
|
||||||
|
|
||||||
|
services.AddAuthorization(options =>
|
||||||
|
{
|
||||||
|
options.AddPolicy("NameMustStartWithB", policy =>
|
||||||
|
policy.RequireAssertion(ctx => ctx.User.Identity.Name?.StartsWith("B") ?? false));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
|
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue