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:
parent
9969e99ef4
commit
bc011b5c97
|
|
@ -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
|
||||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
@page "/PageAllowingAnonymous"
|
||||
@using Microsoft.AspNetCore.Authorization
|
||||
@attribute [Authorize(Roles = "NobodyIsInThisRole")]
|
||||
@attribute [AllowAnonymous]
|
||||
<div id="auth-success">Welcome to PageAllowingAnonymous!</div>
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
@page "/PageRequiringAuthorization"
|
||||
@using Microsoft.AspNetCore.Authorization
|
||||
@attribute [Authorize]
|
||||
<div id="auth-success">Welcome to PageRequiringAuthorization!</div>
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
@page "/PageRequiringPolicy"
|
||||
@using Microsoft.AspNetCore.Authorization
|
||||
@attribute [Authorize(Policy = "NameMustStartWithB")]
|
||||
<div id="auth-success">Welcome to PageRequiringPolicy!</div>
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
@page "/PageRequiringRole"
|
||||
@using Microsoft.AspNetCore.Authorization
|
||||
@attribute [Authorize(Roles = "TestRole")]
|
||||
<div id="auth-success">Welcome to PageRequiringRole!</div>
|
||||
Loading…
Reference in New Issue