Integrate authorization into Blazor router (#10491)

* Split AuthorizeView in two, so "Core" part can be reused from routing

* Rename LayoutDisplay to PageDisplay

* Integrate authorization with Router/PageDisplay

* CR: Replace AuthorizeViewCore.razor with AuthorizeViewCore.cs

* Update tests

* Update ref assemblies

* Add E2E tests

* Update ref assembly exclusions

* More manual ref assembly updating

* Oh these ref assemblies
This commit is contained in:
Steve Sanderson 2019-05-28 02:12:01 +01:00 committed by Ryan Nowak
parent 9969e99ef4
commit bc011b5c97
18 changed files with 502 additions and 232 deletions

View File

@ -5,6 +5,7 @@ T:Microsoft.AspNetCore.Mvc.ApplicationModels.PageParameterModel
T:Microsoft.AspNetCore.Mvc.ApplicationModels.PagePropertyModel
# Manually implemented - https://github.com/aspnet/AspNetCore/issues/8825
T:Microsoft.AspNetCore.Components.AuthorizeView
T:Microsoft.AspNetCore.Components.AuthorizeViewCore
T:Microsoft.AspNetCore.Components.CascadingAuthenticationState
T:Microsoft.AspNetCore.Components.CascadingValue`1
T:Microsoft.AspNetCore.Components.Forms.DataAnnotationsValidator
@ -18,6 +19,6 @@ T:Microsoft.AspNetCore.Components.Forms.InputText
T:Microsoft.AspNetCore.Components.Forms.InputTextArea
T:Microsoft.AspNetCore.Components.Forms.ValidationMessage`1
T:Microsoft.AspNetCore.Components.Forms.ValidationSummary
T:Microsoft.AspNetCore.Components.Layouts.LayoutDisplay
T:Microsoft.AspNetCore.Components.PageDisplay
T:Microsoft.AspNetCore.Components.Routing.NavLink
T:Microsoft.AspNetCore.Components.Routing.Router

View File

@ -49,10 +49,22 @@ namespace Microsoft.AspNetCore.Components.RenderTree
// Built-in components: https://github.com/aspnet/AspNetCore/issues/8825
namespace Microsoft.AspNetCore.Components
{
public partial class AuthorizeView : Microsoft.AspNetCore.Components.ComponentBase
public partial class AuthorizeView : Microsoft.AspNetCore.Components.AuthorizeViewCore
{
public AuthorizeView() { }
[Microsoft.AspNetCore.Components.ParameterAttribute]
public string Policy { [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; } }
[Microsoft.AspNetCore.Components.ParameterAttribute]
public string Roles { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } private set { throw null; } }
protected override Microsoft.AspNetCore.Authorization.IAuthorizeData[] GetAuthorizeData() { throw null; }
}
public abstract partial class AuthorizeViewCore : Microsoft.AspNetCore.Components.ComponentBase
{
public AuthorizeViewCore() { }
[Microsoft.AspNetCore.Components.ParameterAttribute]
public Microsoft.AspNetCore.Components.RenderFragment<Microsoft.AspNetCore.Components.AuthenticationState> Authorized { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } private set { throw null; } }
[Microsoft.AspNetCore.Components.ParameterAttribute]
public Microsoft.AspNetCore.Components.RenderFragment Authorizing { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } private set { throw null; } }
@ -60,13 +72,8 @@ namespace Microsoft.AspNetCore.Components
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<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 abstract Microsoft.AspNetCore.Authorization.IAuthorizeData[] GetAuthorizeData();
[System.Diagnostics.DebuggerStepThroughAttribute]
protected override System.Threading.Tasks.Task OnParametersSetAsync() { throw null; }
}
@ -218,9 +225,13 @@ namespace Microsoft.AspNetCore.Components.Forms
namespace Microsoft.AspNetCore.Components.Layouts
{
public partial class LayoutDisplay : Microsoft.AspNetCore.Components.IComponent
public partial class PageDisplay : Microsoft.AspNetCore.Components.IComponent
{
public LayoutDisplay() { }
public PageDisplay() { }
[Microsoft.AspNetCore.Components.ParameterAttribute]
public Microsoft.AspNetCore.Components.RenderFragment AuthorizingContent { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } private set { throw null; }}
[Microsoft.AspNetCore.Components.ParameterAttribute]
public Microsoft.AspNetCore.Components.RenderFragment<Microsoft.AspNetCore.Components.AuthenticationState> NotAuthorizedContent { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } private set { throw null; }}
[Microsoft.AspNetCore.Components.ParameterAttribute]
public System.Type Page { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } private set { throw null; }}
[Microsoft.AspNetCore.Components.ParameterAttribute]

View File

@ -0,0 +1,42 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Reflection;
using Microsoft.AspNetCore.Authorization;
namespace Microsoft.AspNetCore.Components.Auth
{
internal static class AttributeAuthorizeDataCache
{
private static ConcurrentDictionary<Type, IAuthorizeData[]> _cache
= new ConcurrentDictionary<Type, IAuthorizeData[]>();
public static IAuthorizeData[] GetAuthorizeDataForType(Type type)
{
IAuthorizeData[] result;
if (!_cache.TryGetValue(type, out result))
{
result = ComputeAuthorizeDataForType(type);
_cache[type] = result; // Safe race - doesn't matter if it overwrites
}
return result;
}
private static IAuthorizeData[] ComputeAuthorizeDataForType(Type type)
{
// Allow Anonymous skips all authorization
var allAttributes = type.GetCustomAttributes(inherit: true);
if (allAttributes.OfType<IAllowAnonymous>().Any())
{
return null;
}
var authorizeDataAttributes = allAttributes.OfType<IAuthorizeData>().ToArray();
return authorizeDataAttributes.Length > 0 ? authorizeDataAttributes : null;
}
}
}

View File

@ -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 Microsoft.AspNetCore.Authorization;
namespace Microsoft.AspNetCore.Components
{
/// <summary>
/// Displays differing content depending on the user's authorization status.
/// </summary>
public class AuthorizeView : AuthorizeViewCore
{
private readonly IAuthorizeData[] selfAsAuthorizeData;
/// <summary>
/// Constructs an instance of <see cref="AuthorizeView"/>.
/// </summary>
public AuthorizeView()
{
selfAsAuthorizeData = new[] { new AuthorizeDataAdapter(this) };
}
/// <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>
/// Gets the data used for authorization.
/// </summary>
protected override IAuthorizeData[] GetAuthorizeData()
=> selfAsAuthorizeData;
}
}

View File

@ -1,99 +0,0 @@
@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)
{
@((Authorized ?? ChildContent)?.Invoke(currentAuthenticationState))
}
else
{
@(NotAuthorized?.Invoke(currentAuthenticationState))
}
@functions {
private IAuthorizeData[] selfAsAuthorizeData;
private AuthenticationState currentAuthenticationState;
private bool isAuthorized;
[CascadingParameter] private Task<AuthenticationState> AuthenticationState { get; set; }
/// <summary>
/// The content that will be displayed if the user is authorized.
/// </summary>
[Parameter] public RenderFragment<AuthenticationState> ChildContent { get; private set; }
/// <summary>
/// The content that will be displayed if the user is not authorized.
/// </summary>
[Parameter] public RenderFragment<AuthenticationState> NotAuthorized { get; private set; }
/// <summary>
/// The content that will be displayed if the user is authorized.
/// If you specify a value for this parameter, do not also specify a value for <see cref="ChildContent"/>.
/// </summary>
[Parameter] public RenderFragment<AuthenticationState> Authorized { get; private set; }
/// <summary>
/// The content that will be displayed while asynchronous authorization is in progress.
/// </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
// with 'NotAuthorized' in other cases. Besides naming, they are equivalent. To avoid
// confusion, explicitly prevent the case where both are supplied.
if (ChildContent != null && Authorized != null)
{
throw new InvalidOperationException($"When using {nameof(AuthorizeView)}, do not specify both '{nameof(Authorized)}' and '{nameof(ChildContent)}'.");
}
// First render in pending state
// If the task has already completed, this render will be skipped
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 async Task<bool> IsAuthorizedAsync(ClaimsPrincipal user)
{
var policy = await AuthorizationPolicy.CombineAsync(
AuthorizationPolicyProvider, selfAsAuthorizeData);
var result = await AuthorizationService.AuthorizeAsync(user, Resource, policy);
return result.Succeeded;
}
}

View File

@ -0,0 +1,111 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Components.RenderTree;
namespace Microsoft.AspNetCore.Components
{
/// <summary>
/// A base class for components that display differing content depending on the user's authorization status.
/// </summary>
public abstract class AuthorizeViewCore : ComponentBase
{
private AuthenticationState currentAuthenticationState;
private bool isAuthorized;
/// <summary>
/// The content that will be displayed if the user is authorized.
/// </summary>
[Parameter] public RenderFragment<AuthenticationState> ChildContent { get; private set; }
/// <summary>
/// The content that will be displayed if the user is not authorized.
/// </summary>
[Parameter] public RenderFragment<AuthenticationState> NotAuthorized { get; private set; }
/// <summary>
/// The content that will be displayed if the user is authorized.
/// If you specify a value for this parameter, do not also specify a value for <see cref="ChildContent"/>.
/// </summary>
[Parameter] public RenderFragment<AuthenticationState> Authorized { get; private set; }
/// <summary>
/// The content that will be displayed while asynchronous authorization is in progress.
/// </summary>
[Parameter] public RenderFragment Authorizing { get; private set; }
/// <summary>
/// The resource to which access is being controlled.
/// </summary>
[Parameter] public object Resource { get; private set; }
[CascadingParameter] private Task<AuthenticationState> AuthenticationState { get; set; }
[Inject] private IAuthorizationPolicyProvider AuthorizationPolicyProvider { get; set; }
[Inject] private IAuthorizationService AuthorizationService { get; set; }
/// <inheritdoc />
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
if (currentAuthenticationState == null)
{
builder.AddContent(0, Authorizing);
}
else if (isAuthorized)
{
var authorizedContent = Authorized ?? ChildContent;
builder.AddContent(1, authorizedContent?.Invoke(currentAuthenticationState));
}
else
{
builder.AddContent(2, NotAuthorized?.Invoke(currentAuthenticationState));
}
}
/// <inheritdoc />
protected override async Task OnParametersSetAsync()
{
// We allow 'ChildContent' for convenience in basic cases, and 'Authorized' for symmetry
// with 'NotAuthorized' in other cases. Besides naming, they are equivalent. To avoid
// confusion, explicitly prevent the case where both are supplied.
if (ChildContent != null && Authorized != null)
{
throw new InvalidOperationException($"Do not specify both '{nameof(Authorized)}' and '{nameof(ChildContent)}'.");
}
if (AuthenticationState == null)
{
throw new InvalidOperationException($"Authorization requires a cascading parameter of type Task<{nameof(AuthenticationState)}>. Consider using {typeof(CascadingAuthenticationState).Name} to supply this.");
}
// First render in pending state
// If the task has already completed, this render will be skipped
currentAuthenticationState = null;
// 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);
}
/// <summary>
/// Gets the data required to apply authorization rules.
/// </summary>
protected abstract IAuthorizeData[] GetAuthorizeData();
private async Task<bool> IsAuthorizedAsync(ClaimsPrincipal user)
{
var authorizeData = GetAuthorizeData();
var policy = await AuthorizationPolicy.CombineAsync(
AuthorizationPolicyProvider, authorizeData);
var result = await AuthorizationService.AuthorizeAsync(user, Resource, policy);
return result.Succeeded;
}
}
}

View File

@ -1,90 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.RenderTree;
namespace Microsoft.AspNetCore.Components.Layouts
{
/// <summary>
/// Displays the specified page component, rendering it inside its layout
/// and any further nested layouts.
/// </summary>
public class LayoutDisplay : IComponent
{
internal const string NameOfPage = nameof(Page);
internal const string NameOfPageParameters = nameof(PageParameters);
private RenderHandle _renderHandle;
/// <summary>
/// Gets or sets the type of the page component to display.
/// The type must implement <see cref="IComponent"/>.
/// </summary>
[Parameter]
public Type Page { get; private set; }
/// <summary>
/// Gets or sets the parameters to pass to the page.
/// </summary>
[Parameter]
public IDictionary<string, object> PageParameters { get; private set; }
/// <inheritdoc />
public void Configure(RenderHandle renderHandle)
{
_renderHandle = renderHandle;
}
/// <inheritdoc />
public Task SetParametersAsync(ParameterCollection parameters)
{
parameters.SetParameterProperties(this);
Render();
return Task.CompletedTask;
}
private void Render()
{
// In the middle, we render the requested page
var fragment = RenderComponentWithBody(Page, bodyParam: null);
// Repeatedly wrap it in each layer of nested layout until we get
// to a layout that has no parent
Type layoutType = Page;
while ((layoutType = GetLayoutType(layoutType)) != null)
{
fragment = RenderComponentWithBody(layoutType, fragment);
}
_renderHandle.Render(fragment);
}
private RenderFragment RenderComponentWithBody(Type componentType, RenderFragment bodyParam) => builder =>
{
builder.OpenComponent(0, componentType);
if (bodyParam != null)
{
builder.AddAttribute(1, LayoutComponentBase.BodyPropertyName, bodyParam);
}
else
{
if (PageParameters != null)
{
foreach (var kvp in PageParameters)
{
builder.AddAttribute(1, kvp.Key, kvp.Value);
}
}
}
builder.CloseComponent();
};
private Type GetLayoutType(Type type)
=> type.GetCustomAttribute<LayoutAttribute>()?.LayoutType;
}
}

View File

@ -0,0 +1,140 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Components.Auth;
using Microsoft.AspNetCore.Components.Layouts;
using Microsoft.AspNetCore.Components.RenderTree;
namespace Microsoft.AspNetCore.Components
{
/// <summary>
/// Displays the specified page component, rendering it inside its layout
/// and any further nested layouts, plus applying any authorization rules.
/// </summary>
public class PageDisplay : IComponent
{
private RenderHandle _renderHandle;
/// <summary>
/// Gets or sets the type of the page component to display.
/// The type must implement <see cref="IComponent"/>.
/// </summary>
[Parameter]
public Type Page { get; private set; }
/// <summary>
/// Gets or sets the parameters to pass to the page.
/// </summary>
[Parameter]
public IDictionary<string, object> PageParameters { get; private set; }
/// <summary>
/// The content that will be displayed if the user is not authorized.
/// </summary>
[Parameter]
public RenderFragment<AuthenticationState> NotAuthorizedContent { get; private set; }
/// <summary>
/// The content that will be displayed while asynchronous authorization is in progress.
/// </summary>
[Parameter]
public RenderFragment AuthorizingContent { get; private set; }
/// <inheritdoc />
public void Configure(RenderHandle renderHandle)
{
_renderHandle = renderHandle;
}
/// <inheritdoc />
public Task SetParametersAsync(ParameterCollection parameters)
{
parameters.SetParameterProperties(this);
Render();
return Task.CompletedTask;
}
private void Render()
{
// In the middle goes the requested page
var fragment = (RenderFragment)RenderPageWithParameters;
// Around that goes an AuthorizeViewCore
fragment = WrapInAuthorizeViewCore(fragment);
// Then repeatedly wrap that in each layer of nested layout until we get
// to a layout that has no parent
Type layoutType = Page;
while ((layoutType = GetLayoutType(layoutType)) != null)
{
fragment = WrapInLayout(layoutType, fragment);
}
_renderHandle.Render(fragment);
}
private RenderFragment WrapInLayout(Type layoutType, RenderFragment bodyParam) => builder =>
{
builder.OpenComponent(0, layoutType);
builder.AddAttribute(1, LayoutComponentBase.BodyPropertyName, bodyParam);
builder.CloseComponent();
};
private void RenderPageWithParameters(RenderTreeBuilder builder)
{
builder.OpenComponent(0, Page);
if (PageParameters != null)
{
foreach (var kvp in PageParameters)
{
builder.AddAttribute(1, kvp.Key, kvp.Value);
}
}
builder.CloseComponent();
}
private RenderFragment WrapInAuthorizeViewCore(RenderFragment pageFragment)
{
var authorizeData = AttributeAuthorizeDataCache.GetAuthorizeDataForType(Page);
if (authorizeData == null)
{
// No authorization, so no need to wrap the fragment
return pageFragment;
}
// Some authorization data exists, so we do need to wrap the fragment
RenderFragment<AuthenticationState> authorizedContent = context => pageFragment;
return builder =>
{
builder.OpenComponent<AuthorizeViewWithSuppliedData>(0);
builder.AddAttribute(1, nameof(AuthorizeViewWithSuppliedData.AuthorizeDataParam), authorizeData);
builder.AddAttribute(2, nameof(AuthorizeViewWithSuppliedData.Authorized), authorizedContent);
builder.AddAttribute(3, nameof(AuthorizeViewWithSuppliedData.NotAuthorized), NotAuthorizedContent ?? DefaultNotAuthorizedContent);
builder.AddAttribute(4, nameof(AuthorizeViewWithSuppliedData.Authorizing), AuthorizingContent);
builder.CloseComponent();
};
}
private static Type GetLayoutType(Type type)
=> type.GetCustomAttribute<LayoutAttribute>()?.LayoutType;
private class AuthorizeViewWithSuppliedData : AuthorizeViewCore
{
[Parameter] public IAuthorizeData[] AuthorizeDataParam { get; private set; }
protected override IAuthorizeData[] GetAuthorizeData() => AuthorizeDataParam;
}
// There has to be some default content. If we render blank by default, developers
// will find it hard to guess why their UI isn't appearing.
private static RenderFragment DefaultNotAuthorizedContent(AuthenticationState authenticationState)
=> builder => builder.AddContent(0, "Not authorized");
}
}

View File

@ -40,6 +40,16 @@ namespace Microsoft.AspNetCore.Components.Routing
/// </summary>
[Parameter] public RenderFragment NotFoundContent { get; private set; }
/// <summary>
/// The content that will be displayed if the user is not authorized.
/// </summary>
[Parameter] public RenderFragment<AuthenticationState> NotAuthorizedContent { get; private set; }
/// <summary>
/// The content that will be displayed while asynchronous authorization is in progress.
/// </summary>
[Parameter] public RenderFragment AuthorizingContent { get; private set; }
private RouteTable Routes { get; set; }
/// <inheritdoc />
@ -78,9 +88,11 @@ namespace Microsoft.AspNetCore.Components.Routing
/// <inheritdoc />
protected virtual void Render(RenderTreeBuilder builder, Type handler, IDictionary<string, object> parameters)
{
builder.OpenComponent(0, typeof(LayoutDisplay));
builder.AddAttribute(1, LayoutDisplay.NameOfPage, handler);
builder.AddAttribute(2, LayoutDisplay.NameOfPageParameters, parameters);
builder.OpenComponent(0, typeof(PageDisplay));
builder.AddAttribute(1, nameof(PageDisplay.Page), handler);
builder.AddAttribute(2, nameof(PageDisplay.PageParameters), parameters);
builder.AddAttribute(3, nameof(PageDisplay.NotAuthorizedContent), NotAuthorizedContent);
builder.AddAttribute(4, nameof(PageDisplay.AuthorizingContent), AuthorizingContent);
builder.CloseComponent();
}

View File

@ -248,7 +248,7 @@ namespace Microsoft.AspNetCore.Components
renderer.AssignRootComponentId(rootComponent);
var ex = Assert.Throws<InvalidOperationException>(() =>
rootComponent.TriggerRender());
Assert.Equal("When using AuthorizeView, do not specify both 'Authorized' and 'ChildContent'.", ex.Message);
Assert.Equal("Do not specify both 'Authorized' and 'ChildContent'.", ex.Message);
}
[Fact]

View File

@ -11,26 +11,26 @@ using Xunit;
namespace Microsoft.AspNetCore.Components.Test
{
public class LayoutTest
public class PageDisplayTest
{
private TestRenderer _renderer = new TestRenderer();
private LayoutDisplay _layoutDisplayComponent = new LayoutDisplay();
private int _layoutDisplayComponentId;
private PageDisplay _pageDisplayComponent = new PageDisplay();
private int _pageDisplayComponentId;
public LayoutTest()
public PageDisplayTest()
{
_renderer = new TestRenderer();
_layoutDisplayComponent = new LayoutDisplay();
_layoutDisplayComponentId = _renderer.AssignRootComponentId(_layoutDisplayComponent);
_pageDisplayComponent = new PageDisplay();
_pageDisplayComponentId = _renderer.AssignRootComponentId(_pageDisplayComponent);
}
[Fact]
public void DisplaysComponentInsideLayout()
{
// Arrange/Act
_renderer.Invoke(() => _layoutDisplayComponent.SetParametersAsync(ParameterCollection.FromDictionary(new Dictionary<string, object>
_renderer.Invoke(() => _pageDisplayComponent.SetParametersAsync(ParameterCollection.FromDictionary(new Dictionary<string, object>
{
{ LayoutDisplay.NameOfPage, typeof(ComponentWithLayout) }
{ nameof(PageDisplay.Page), typeof(ComponentWithLayout) }
})));
// Assert
@ -85,9 +85,9 @@ namespace Microsoft.AspNetCore.Components.Test
public void DisplaysComponentInsideNestedLayout()
{
// Arrange/Act
_renderer.Invoke(() => _layoutDisplayComponent.SetParametersAsync(ParameterCollection.FromDictionary(new Dictionary<string, object>
_renderer.Invoke(() => _pageDisplayComponent.SetParametersAsync(ParameterCollection.FromDictionary(new Dictionary<string, object>
{
{ LayoutDisplay.NameOfPage, typeof(ComponentWithNestedLayout) }
{ nameof(PageDisplay.Page), typeof(ComponentWithNestedLayout) }
})));
// Assert
@ -112,15 +112,15 @@ namespace Microsoft.AspNetCore.Components.Test
public void CanChangeDisplayedPageWithSameLayout()
{
// Arrange
_renderer.Invoke(() => _layoutDisplayComponent.SetParametersAsync(ParameterCollection.FromDictionary(new Dictionary<string, object>
_renderer.Invoke(() => _pageDisplayComponent.SetParametersAsync(ParameterCollection.FromDictionary(new Dictionary<string, object>
{
{ LayoutDisplay.NameOfPage, typeof(ComponentWithLayout) }
{ nameof(PageDisplay.Page), typeof(ComponentWithLayout) }
})));
// Act
_renderer.Invoke(() => _layoutDisplayComponent.SetParametersAsync(ParameterCollection.FromDictionary(new Dictionary<string, object>
_renderer.Invoke(() => _pageDisplayComponent.SetParametersAsync(ParameterCollection.FromDictionary(new Dictionary<string, object>
{
{ LayoutDisplay.NameOfPage, typeof(DifferentComponentWithLayout) }
{ nameof(PageDisplay.Page), typeof(DifferentComponentWithLayout) }
})));
// Assert
@ -163,15 +163,15 @@ namespace Microsoft.AspNetCore.Components.Test
public void CanChangeDisplayedPageWithDifferentLayout()
{
// Arrange
_renderer.Invoke(() => _layoutDisplayComponent.SetParametersAsync(ParameterCollection.FromDictionary(new Dictionary<string, object>
_renderer.Invoke(() => _pageDisplayComponent.SetParametersAsync(ParameterCollection.FromDictionary(new Dictionary<string, object>
{
{ LayoutDisplay.NameOfPage, typeof(ComponentWithLayout) }
{ nameof(PageDisplay.Page), typeof(ComponentWithLayout) }
})));
// Act
_renderer.Invoke(() => _layoutDisplayComponent.SetParametersAsync(ParameterCollection.FromDictionary(new Dictionary<string, object>
_renderer.Invoke(() => _pageDisplayComponent.SetParametersAsync(ParameterCollection.FromDictionary(new Dictionary<string, object>
{
{ LayoutDisplay.NameOfPage, typeof(ComponentWithNestedLayout) }
{ nameof(PageDisplay.Page), typeof(ComponentWithNestedLayout) }
})));
// Assert

View File

@ -16,6 +16,10 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
// These strings correspond to the links in BasicTestApp\AuthTest\Links.razor
const string CascadingAuthenticationStateLink = "Cascading authentication state";
const string AuthorizeViewCases = "AuthorizeView cases";
const string PageAllowingAnonymous = "Page allowing anonymous";
const string PageRequiringAuthorization = "Page requiring any authentication";
const string PageRequiringPolicy = "Page requiring policy";
const string PageRequiringRole = "Page requiring role";
public AuthTest(
BrowserFixture browserFixture,
@ -54,7 +58,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
}
[Fact]
public void AuthorizeViewCases_NoAuthorizationRule_Unauthenticated()
public void AuthorizeViewCases_NoAuthorizationRule_NotAuthorized()
{
SignInAs(null, null);
var appElement = MountAndNavigateToAuthTest(AuthorizeViewCases);
@ -64,7 +68,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
}
[Fact]
public void AuthorizeViewCases_NoAuthorizationRule_Authenticated()
public void AuthorizeViewCases_NoAuthorizationRule_Authorized()
{
SignInAs("Some User", null);
var appElement = MountAndNavigateToAuthTest(AuthorizeViewCases);
@ -73,7 +77,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
}
[Fact]
public void AuthorizeViewCases_RequireRole_Authenticated()
public void AuthorizeViewCases_RequireRole_Authorized()
{
SignInAs("Some User", "IrrelevantRole,TestRole");
var appElement = MountAndNavigateToAuthTest(AuthorizeViewCases);
@ -82,7 +86,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
}
[Fact]
public void AuthorizeViewCases_RequireRole_Unauthenticated()
public void AuthorizeViewCases_RequireRole_NotAuthorized()
{
SignInAs("Some User", "IrrelevantRole");
var appElement = MountAndNavigateToAuthTest(AuthorizeViewCases);
@ -91,7 +95,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
}
[Fact]
public void AuthorizeViewCases_RequirePolicy_Authenticated()
public void AuthorizeViewCases_RequirePolicy_Authorized()
{
SignInAs("Bert", null);
var appElement = MountAndNavigateToAuthTest(AuthorizeViewCases);
@ -100,7 +104,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
}
[Fact]
public void AuthorizeViewCases_RequirePolicy_Unauthenticated()
public void AuthorizeViewCases_RequirePolicy_NotAuthorized()
{
SignInAs("Mallory", null);
var appElement = MountAndNavigateToAuthTest(AuthorizeViewCases);
@ -108,6 +112,78 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
appElement.FindElement(By.CssSelector("#authorize-policy .not-authorized")).Text);
}
[Fact]
public void Router_AllowAnonymous_Anonymous()
{
SignInAs(null, null);
var appElement = MountAndNavigateToAuthTest(PageAllowingAnonymous);
Browser.Equal("Welcome to PageAllowingAnonymous!", () =>
appElement.FindElement(By.CssSelector("#auth-success")).Text);
}
[Fact]
public void Router_AllowAnonymous_Authenticated()
{
SignInAs("Bert", null);
var appElement = MountAndNavigateToAuthTest(PageAllowingAnonymous);
Browser.Equal("Welcome to PageAllowingAnonymous!", () =>
appElement.FindElement(By.CssSelector("#auth-success")).Text);
}
[Fact]
public void Router_RequireAuthorization_Authorized()
{
SignInAs("Bert", null);
var appElement = MountAndNavigateToAuthTest(PageRequiringAuthorization);
Browser.Equal("Welcome to PageRequiringAuthorization!", () =>
appElement.FindElement(By.CssSelector("#auth-success")).Text);
}
[Fact]
public void Router_RequireAuthorization_NotAuthorized()
{
SignInAs(null, null);
var appElement = MountAndNavigateToAuthTest(PageRequiringAuthorization);
Browser.Equal("Sorry, anonymous, you're not authorized.", () =>
appElement.FindElement(By.CssSelector("#auth-failure")).Text);
}
[Fact]
public void Router_RequirePolicy_Authorized()
{
SignInAs("Bert", null);
var appElement = MountAndNavigateToAuthTest(PageRequiringPolicy);
Browser.Equal("Welcome to PageRequiringPolicy!", () =>
appElement.FindElement(By.CssSelector("#auth-success")).Text);
}
[Fact]
public void Router_RequirePolicy_NotAuthorized()
{
SignInAs("Mallory", null);
var appElement = MountAndNavigateToAuthTest(PageRequiringPolicy);
Browser.Equal("Sorry, Mallory, you're not authorized.", () =>
appElement.FindElement(By.CssSelector("#auth-failure")).Text);
}
[Fact]
public void Router_RequireRole_Authorized()
{
SignInAs("Bert", "IrrelevantRole,TestRole");
var appElement = MountAndNavigateToAuthTest(PageRequiringRole);
Browser.Equal("Welcome to PageRequiringRole!", () =>
appElement.FindElement(By.CssSelector("#auth-success")).Text);
}
[Fact]
public void Router_RequireRole_NotAuthorized()
{
SignInAs("Bert", "IrrelevantRole");
var appElement = MountAndNavigateToAuthTest(PageRequiringRole);
Browser.Equal("Sorry, Bert, you're not authorized.", () =>
appElement.FindElement(By.CssSelector("#auth-failure")).Text);
}
IWebElement MountAndNavigateToAuthTest(string authLinkText)
{
Navigate(ServerPathBase);

View File

@ -9,7 +9,14 @@
*@
<CascadingAuthenticationState>
<Router AppAssembly=typeof(BasicTestApp.Program).Assembly />
<Router AppAssembly=typeof(BasicTestApp.Program).Assembly>
<AuthorizingContent>Authorizing...</AuthorizingContent>
<NotAuthorizedContent>
<div id="auth-failure">
Sorry, @(context.User.Identity.Name ?? "anonymous"), you're not authorized.
</div>
</NotAuthorizedContent>
</Router>
</CascadingAuthenticationState>
<hr />

View File

@ -1,8 +1,11 @@
@using Microsoft.AspNetCore.Components.Routing
<ul id="auth-links">
<li><NavLink href="AuthHome" Match=NavLinkMatch.All>Home</NavLink></li>
<li><NavLink href="CascadingAuthenticationStateConsumer">Cascading authentication state</NavLink></li>
<li><NavLink href="AuthorizeViewCases">AuthorizeView cases</NavLink></li>
<li><a href="AuthHome">Home</a></li>
<li><a href="CascadingAuthenticationStateConsumer">Cascading authentication state</a></li>
<li><a href="AuthorizeViewCases">AuthorizeView cases</a></li>
<li><a href="PageAllowingAnonymous">Page allowing anonymous</a></li>
<li><a href="PageRequiringAuthorization">Page requiring any authentication</a></li>
<li><a href="PageRequiringPolicy">Page requiring policy</a></li>
<li><a href="PageRequiringRole">Page requiring role</a></li>
</ul>
<p>To change the underlying authentication state, <a target="_blank" href="/Authentication">go here</a>.</p>

View File

@ -0,0 +1,5 @@
@page "/PageAllowingAnonymous"
@using Microsoft.AspNetCore.Authorization
@attribute [Authorize(Roles = "NobodyIsInThisRole")]
@attribute [AllowAnonymous]
<div id="auth-success">Welcome to PageAllowingAnonymous!</div>

View File

@ -0,0 +1,4 @@
@page "/PageRequiringAuthorization"
@using Microsoft.AspNetCore.Authorization
@attribute [Authorize]
<div id="auth-success">Welcome to PageRequiringAuthorization!</div>

View File

@ -0,0 +1,4 @@
@page "/PageRequiringPolicy"
@using Microsoft.AspNetCore.Authorization
@attribute [Authorize(Policy = "NameMustStartWithB")]
<div id="auth-success">Welcome to PageRequiringPolicy!</div>

View File

@ -0,0 +1,4 @@
@page "/PageRequiringRole"
@using Microsoft.AspNetCore.Authorization
@attribute [Authorize(Roles = "TestRole")]
<div id="auth-success">Welcome to PageRequiringRole!</div>