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.Routing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
namespace Microsoft.AspNetCore.Blazor.Hosting
|
||||
|
|
@ -92,6 +94,7 @@ namespace Microsoft.AspNetCore.Blazor.Hosting
|
|||
services.AddSingleton<IComponentContext, WebAssemblyComponentContext>();
|
||||
services.AddSingleton<IUriHelper>(WebAssemblyUriHelper.Instance);
|
||||
services.AddSingleton<INavigationInterception>(WebAssemblyNavigationInterception.Instance);
|
||||
services.AddSingleton<ILoggerFactory, WebAssemblyLoggerFactory>();
|
||||
services.AddSingleton<HttpClient>(s =>
|
||||
{
|
||||
// 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)
|
||||
{
|
||||
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>
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
|
||||
<Compile Include="Microsoft.AspNetCore.Components.netstandard2.0.cs" />
|
||||
<Reference Include="Microsoft.AspNetCore.Authorization" />
|
||||
<Reference Include="Microsoft.JSInterop" />
|
||||
<Reference Include="System.ComponentModel.Annotations" />
|
||||
</ItemGroup>
|
||||
|
|
|
|||
|
|
@ -59,7 +59,13 @@ namespace Microsoft.AspNetCore.Components
|
|||
[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; } }
|
||||
[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) { }
|
||||
[System.Diagnostics.DebuggerStepThroughAttribute]
|
||||
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
|
||||
@using System.Security.Claims
|
||||
@using Microsoft.AspNetCore.Authorization
|
||||
@inject IAuthorizationService AuthorizationService
|
||||
@inject IAuthorizationPolicyProvider AuthorizationPolicyProvider
|
||||
|
||||
@if (currentAuthenticationState == null)
|
||||
{
|
||||
@Authorizing
|
||||
}
|
||||
else if (IsAuthorized())
|
||||
else if (isAuthorized)
|
||||
{
|
||||
@((Authorized ?? ChildContent)?.Invoke(currentAuthenticationState))
|
||||
}
|
||||
else
|
||||
{
|
||||
@NotAuthorized
|
||||
@(NotAuthorized?.Invoke(currentAuthenticationState))
|
||||
}
|
||||
|
||||
@functions {
|
||||
private IAuthorizeData[] selfAsAuthorizeData;
|
||||
private AuthenticationState currentAuthenticationState;
|
||||
private bool isAuthorized;
|
||||
|
||||
[CascadingParameter] private Task<AuthenticationState> AuthenticationState { get; set; }
|
||||
|
||||
|
|
@ -26,7 +32,7 @@ else
|
|||
/// <summary>
|
||||
/// The content that will be displayed if the user is not authorized.
|
||||
/// </summary>
|
||||
[Parameter] public RenderFragment NotAuthorized { get; private set; }
|
||||
[Parameter] public RenderFragment<AuthenticationState> NotAuthorized { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The content that will be displayed if the user is authorized.
|
||||
|
|
@ -39,6 +45,29 @@ else
|
|||
/// </summary>
|
||||
[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()
|
||||
{
|
||||
// We allow 'ChildContent' for convenience in basic cases, and 'Authorized' for symmetry
|
||||
|
|
@ -54,15 +83,17 @@ else
|
|||
currentAuthenticationState = 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);
|
||||
}
|
||||
|
||||
private bool IsAuthorized()
|
||||
private async Task<bool> IsAuthorizedAsync(ClaimsPrincipal user)
|
||||
{
|
||||
// TODO: Support various authorization condition parameters, equivalent to those offered
|
||||
// by the [Authorize] attribute, e.g., "Roles" and "Policy". This is on hold until we're
|
||||
// able to reference the policy evaluator APIs from this package.
|
||||
|
||||
return currentAuthenticationState.User?.Identity?.IsAuthenticated == true;
|
||||
var policy = await AuthorizationPolicy.CombineAsync(
|
||||
AuthorizationPolicyProvider, selfAsAuthorizeData);
|
||||
var result = await AuthorizationService.AuthorizeAsync(user, Resource, policy);
|
||||
return result.Succeeded;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.AspNetCore.Authorization" />
|
||||
<Reference Include="Microsoft.JSInterop" />
|
||||
<Reference Include="System.ComponentModel.Annotations" />
|
||||
</ItemGroup>
|
||||
|
|
|
|||
|
|
@ -2,14 +2,18 @@
|
|||
// 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.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Security.Principal;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Authorization.Infrastructure;
|
||||
using Microsoft.AspNetCore.Components.RenderTree;
|
||||
using Microsoft.AspNetCore.Components.Test.Helpers;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components
|
||||
|
|
@ -24,7 +28,8 @@ namespace Microsoft.AspNetCore.Components
|
|||
public void RendersNothingIfNotAuthorized()
|
||||
{
|
||||
// Arrange
|
||||
var renderer = new TestRenderer();
|
||||
var authorizationService = new TestAuthorizationService();
|
||||
var renderer = CreateTestRenderer(authorizationService);
|
||||
var rootComponent = WrapInAuthorizeView(
|
||||
childContent:
|
||||
context => builder => builder.AddContent(0, "This should not be rendered"));
|
||||
|
|
@ -36,18 +41,27 @@ namespace Microsoft.AspNetCore.Components
|
|||
// Assert
|
||||
var diff = renderer.Batches.Single().GetComponentDiffs<AuthorizeView>().Single();
|
||||
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]
|
||||
public void RendersNotAuthorizedContentIfNotAuthorized()
|
||||
{
|
||||
// Arrange
|
||||
var renderer = new TestRenderer();
|
||||
var authorizationService = new TestAuthorizationService();
|
||||
var renderer = CreateTestRenderer(authorizationService);
|
||||
var rootComponent = WrapInAuthorizeView(
|
||||
childContent:
|
||||
context => builder => builder.AddContent(0, "This should not be rendered"),
|
||||
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
|
||||
renderer.AssignRootComponentId(rootComponent);
|
||||
|
|
@ -60,7 +74,16 @@ namespace Microsoft.AspNetCore.Components
|
|||
Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
|
||||
AssertFrame.Text(
|
||||
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()
|
||||
{
|
||||
// Arrange
|
||||
var renderer = new TestRenderer();
|
||||
var authorizationService = new TestAuthorizationService();
|
||||
authorizationService.NextResult = AuthorizationResult.Success();
|
||||
var renderer = CreateTestRenderer(authorizationService);
|
||||
var rootComponent = WrapInAuthorizeView();
|
||||
rootComponent.AuthenticationState = CreateAuthenticationState("Nellie");
|
||||
|
||||
|
|
@ -79,13 +104,24 @@ namespace Microsoft.AspNetCore.Components
|
|||
// Assert
|
||||
var diff = renderer.Batches.Single().GetComponentDiffs<AuthorizeView>().Single();
|
||||
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]
|
||||
public void RendersChildContentIfAuthorized()
|
||||
{
|
||||
// Arrange
|
||||
var renderer = new TestRenderer();
|
||||
var authorizationService = new TestAuthorizationService();
|
||||
authorizationService.NextResult = AuthorizationResult.Success();
|
||||
var renderer = CreateTestRenderer(authorizationService);
|
||||
var rootComponent = WrapInAuthorizeView(
|
||||
childContent: context => builder =>
|
||||
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],
|
||||
"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]
|
||||
public void RendersAuthorizedContentIfAuthorized()
|
||||
{
|
||||
// Arrange
|
||||
var renderer = new TestRenderer();
|
||||
var authorizationService = new TestAuthorizationService();
|
||||
authorizationService.NextResult = AuthorizationResult.Success();
|
||||
var renderer = CreateTestRenderer(authorizationService);
|
||||
var rootComponent = WrapInAuthorizeView(
|
||||
authorizedContent: context => builder =>
|
||||
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],
|
||||
"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]
|
||||
public void RespondsToChangeInAuthorizationState()
|
||||
{
|
||||
// Arrange
|
||||
var renderer = new TestRenderer();
|
||||
var authorizationService = new TestAuthorizationService();
|
||||
authorizationService.NextResult = AuthorizationResult.Success();
|
||||
var renderer = CreateTestRenderer(authorizationService);
|
||||
var rootComponent = WrapInAuthorizeView(
|
||||
childContent: context => builder =>
|
||||
builder.AddContent(0, $"You are authenticated as {context.User.Identity.Name}"));
|
||||
|
|
@ -147,6 +205,7 @@ namespace Microsoft.AspNetCore.Components
|
|||
rootComponent.TriggerRender();
|
||||
var authorizeViewComponentId = renderer.Batches.Single()
|
||||
.GetComponentFrames<AuthorizeView>().Single().ComponentId;
|
||||
authorizationService.AuthorizeCalls.Clear();
|
||||
|
||||
// Act
|
||||
rootComponent.AuthenticationState = CreateAuthenticationState("Ronaldo");
|
||||
|
|
@ -164,13 +223,23 @@ namespace Microsoft.AspNetCore.Components
|
|||
batch.ReferenceFrames[edit.ReferenceFrameIndex],
|
||||
"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]
|
||||
public void ThrowsIfBothChildContentAndAuthorizedContentProvided()
|
||||
{
|
||||
// Arrange
|
||||
var renderer = new TestRenderer();
|
||||
var authorizationService = new TestAuthorizationService();
|
||||
var renderer = CreateTestRenderer(authorizationService);
|
||||
var rootComponent = WrapInAuthorizeView(
|
||||
authorizedContent: context => builder => { },
|
||||
childContent: context => builder => { });
|
||||
|
|
@ -187,12 +256,12 @@ namespace Microsoft.AspNetCore.Components
|
|||
{
|
||||
// Arrange
|
||||
var @event = new ManualResetEventSlim();
|
||||
var renderer = new TestRenderer()
|
||||
{
|
||||
OnUpdateDisplayComplete = () => { @event.Set(); },
|
||||
};
|
||||
var authorizationService = new TestAuthorizationService();
|
||||
var renderer = CreateTestRenderer(authorizationService);
|
||||
renderer.OnUpdateDisplayComplete = () => { @event.Set(); };
|
||||
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>();
|
||||
rootComponent.AuthenticationState = authTcs.Task;
|
||||
|
||||
|
|
@ -228,10 +297,10 @@ namespace Microsoft.AspNetCore.Components
|
|||
{
|
||||
// Arrange
|
||||
var @event = new ManualResetEventSlim();
|
||||
var renderer = new TestRenderer()
|
||||
{
|
||||
OnUpdateDisplayComplete = () => { @event.Set(); },
|
||||
};
|
||||
var authorizationService = new TestAuthorizationService();
|
||||
authorizationService.NextResult = AuthorizationResult.Success();
|
||||
var renderer = CreateTestRenderer(authorizationService);
|
||||
renderer.OnUpdateDisplayComplete = () => { @event.Set(); };
|
||||
var rootComponent = WrapInAuthorizeView(
|
||||
authorizingContent: builder => builder.AddContent(0, "Auth pending..."),
|
||||
authorizedContent: context => builder => builder.AddContent(0, $"Hello, {context.User.Identity.Name}!"));
|
||||
|
|
@ -276,13 +345,96 @@ namespace Microsoft.AspNetCore.Components
|
|||
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 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(
|
||||
RenderFragment<AuthenticationState> childContent = null,
|
||||
RenderFragment<AuthenticationState> authorizedContent = null,
|
||||
RenderFragment notAuthorizedContent = null,
|
||||
RenderFragment authorizingContent = null)
|
||||
RenderFragment<AuthenticationState> notAuthorizedContent = null,
|
||||
RenderFragment authorizingContent = null,
|
||||
string policy = null,
|
||||
string roles = null,
|
||||
object resource = null)
|
||||
{
|
||||
return new TestAuthStateProviderComponent(builder =>
|
||||
{
|
||||
|
|
@ -291,6 +443,9 @@ namespace Microsoft.AspNetCore.Components
|
|||
builder.AddAttribute(2, nameof(AuthorizeView.Authorized), authorizedContent);
|
||||
builder.AddAttribute(3, nameof(AuthorizeView.NotAuthorized), notAuthorizedContent);
|
||||
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();
|
||||
});
|
||||
}
|
||||
|
|
@ -311,11 +466,31 @@ namespace Microsoft.AspNetCore.Components
|
|||
{
|
||||
builder.OpenComponent<CascadingValue<Task<AuthenticationState>>>(0);
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
=> Task.FromResult(new AuthenticationState(
|
||||
new ClaimsPrincipal(new TestIdentity { Name = username })));
|
||||
|
|
@ -328,5 +503,59 @@ namespace Microsoft.AspNetCore.Components
|
|||
|
||||
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)
|
||||
{
|
||||
throw exception;
|
||||
ExceptionDispatchInfo.Capture(exception).Throw();
|
||||
}
|
||||
|
||||
HandledExceptions.Add(exception);
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
|||
[Fact]
|
||||
public void CascadingAuthenticationState_Unauthenticated()
|
||||
{
|
||||
SignInAs(null);
|
||||
SignInAs(null, null);
|
||||
|
||||
var appElement = MountAndNavigateToAuthTest(CascadingAuthenticationStateLink);
|
||||
|
||||
|
|
@ -44,7 +44,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
|||
[Fact]
|
||||
public void CascadingAuthenticationState_Authenticated()
|
||||
{
|
||||
SignInAs("someone cool");
|
||||
SignInAs("someone cool", null);
|
||||
|
||||
var appElement = MountAndNavigateToAuthTest(CascadingAuthenticationStateLink);
|
||||
|
||||
|
|
@ -56,20 +56,58 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
|||
[Fact]
|
||||
public void AuthorizeViewCases_NoAuthorizationRule_Unauthenticated()
|
||||
{
|
||||
SignInAs(null);
|
||||
MountAndNavigateToAuthTest(AuthorizeViewCases);
|
||||
SignInAs(null, null);
|
||||
var appElement = MountAndNavigateToAuthTest(AuthorizeViewCases);
|
||||
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]
|
||||
public void AuthorizeViewCases_NoAuthorizationRule_Authenticated()
|
||||
{
|
||||
SignInAs("Some User");
|
||||
SignInAs("Some User", null);
|
||||
var appElement = MountAndNavigateToAuthTest(AuthorizeViewCases);
|
||||
Browser.Equal("Welcome, Some User!", () =>
|
||||
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)
|
||||
{
|
||||
Navigate(ServerPathBase);
|
||||
|
|
@ -79,12 +117,12 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
|||
return appElement;
|
||||
}
|
||||
|
||||
void SignInAs(string usernameOrNull)
|
||||
void SignInAs(string usernameOrNull, string rolesOrNull)
|
||||
{
|
||||
const string authenticationPageUrl = "/Authentication";
|
||||
var baseRelativeUri = usernameOrNull == null
|
||||
? $"{authenticationPageUrl}?signout=true"
|
||||
: $"{authenticationPageUrl}?username={usernameOrNull}";
|
||||
: $"{authenticationPageUrl}?username={usernameOrNull}&roles={rolesOrNull}";
|
||||
Navigate(baseRelativeUri);
|
||||
WaitUntilExists(By.CssSelector("h1#authentication"));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,39 @@
|
|||
<p class="authorized">Welcome, @context.User.Identity.Name!</p>
|
||||
</Authorized>
|
||||
<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>
|
||||
</AuthorizeView>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -12,6 +12,6 @@ namespace BasicTestApp.AuthTest
|
|||
|
||||
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)
|
||||
{
|
||||
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");
|
||||
}
|
||||
else
|
||||
|
|
|
|||
|
|
@ -15,6 +15,12 @@ namespace BasicTestApp
|
|||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using BasicTestApp.AuthTest;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
|
|
@ -7,22 +9,28 @@ namespace Components.TestServer.Controllers
|
|||
[Route("api/[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
|
||||
[HttpGet]
|
||||
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
|
||||
{
|
||||
IsAuthenticated = User.Identity.IsAuthenticated,
|
||||
UserName = User.Identity.Name,
|
||||
ExposedClaims = User.Claims
|
||||
.Where(c => c.Type == "test-claim")
|
||||
.ToDictionary(c => c.Type, c => c.Value)
|
||||
.Where(c => c.Type == "test-claim" || IsExposedRole(c))
|
||||
.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:
|
||||
<input name="username" />
|
||||
</p>
|
||||
<p>
|
||||
Roles:
|
||||
<input name="roles" />
|
||||
</p>
|
||||
<p>
|
||||
<button type="submit">Submit</button>
|
||||
</p>
|
||||
|
|
@ -37,7 +41,11 @@
|
|||
<p>
|
||||
Authenticated: <strong>@User.Identity.IsAuthenticated</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>
|
||||
</fieldset>
|
||||
</body>
|
||||
|
|
@ -61,6 +69,15 @@
|
|||
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(
|
||||
new ClaimsPrincipal(new ClaimsIdentity(claims, "FakeAuthenticationType")));
|
||||
|
||||
|
|
|
|||
|
|
@ -30,6 +30,12 @@ namespace TestServer
|
|||
});
|
||||
services.AddServerSideBlazor();
|
||||
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.
|
||||
|
|
|
|||
Loading…
Reference in New Issue