From bc011b5c973018f333ee5eaa367f531883a1e36e Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Tue, 28 May 2019 02:12:01 +0100 Subject: [PATCH] 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 --- eng/GenAPI.exclusions.txt | 3 +- ...etCore.Components.netstandard2.0.Manual.cs | 29 ++-- .../src/Auth/AttributeAuthorizeDataCache.cs | 42 ++++++ .../Components/src/Auth/AuthorizeView.cs | 39 +++++ .../Components/src/Auth/AuthorizeView.razor | 99 ------------- .../Components/src/Auth/AuthorizeViewCore.cs | 111 ++++++++++++++ .../Components/src/Layouts/LayoutDisplay.cs | 90 ----------- .../Components/src/Layouts/PageDisplay.cs | 140 ++++++++++++++++++ .../Components/src/Routing/Router.cs | 18 ++- .../Components/test/Auth/AuthorizeViewTest.cs | 2 +- .../{LayoutTest.cs => PageDisplayTest.cs} | 36 ++--- src/Components/test/E2ETest/Tests/AuthTest.cs | 88 ++++++++++- .../BasicTestApp/AuthTest/AuthRouter.razor | 9 +- .../BasicTestApp/AuthTest/Links.razor | 11 +- .../AuthTest/PageAllowingAnonymous.razor | 5 + .../AuthTest/PageRequiringAuthorization.razor | 4 + .../AuthTest/PageRequiringPolicy.razor | 4 + .../AuthTest/PageRequiringRole.razor | 4 + 18 files changed, 502 insertions(+), 232 deletions(-) create mode 100644 src/Components/Components/src/Auth/AttributeAuthorizeDataCache.cs create mode 100644 src/Components/Components/src/Auth/AuthorizeView.cs delete mode 100644 src/Components/Components/src/Auth/AuthorizeView.razor create mode 100644 src/Components/Components/src/Auth/AuthorizeViewCore.cs delete mode 100644 src/Components/Components/src/Layouts/LayoutDisplay.cs create mode 100644 src/Components/Components/src/Layouts/PageDisplay.cs rename src/Components/Components/test/{LayoutTest.cs => PageDisplayTest.cs} (86%) create mode 100644 src/Components/test/testassets/BasicTestApp/AuthTest/PageAllowingAnonymous.razor create mode 100644 src/Components/test/testassets/BasicTestApp/AuthTest/PageRequiringAuthorization.razor create mode 100644 src/Components/test/testassets/BasicTestApp/AuthTest/PageRequiringPolicy.razor create mode 100644 src/Components/test/testassets/BasicTestApp/AuthTest/PageRequiringRole.razor diff --git a/eng/GenAPI.exclusions.txt b/eng/GenAPI.exclusions.txt index 4595fc772a..3053eb79e1 100644 --- a/eng/GenAPI.exclusions.txt +++ b/eng/GenAPI.exclusions.txt @@ -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 \ No newline at end of file diff --git a/src/Components/Components/ref/Microsoft.AspNetCore.Components.netstandard2.0.Manual.cs b/src/Components/Components/ref/Microsoft.AspNetCore.Components.netstandard2.0.Manual.cs index 41b4d9a028..357b78e82a 100644 --- a/src/Components/Components/ref/Microsoft.AspNetCore.Components.netstandard2.0.Manual.cs +++ b/src/Components/Components/ref/Microsoft.AspNetCore.Components.netstandard2.0.Manual.cs @@ -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 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 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; } } - [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 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] diff --git a/src/Components/Components/src/Auth/AttributeAuthorizeDataCache.cs b/src/Components/Components/src/Auth/AttributeAuthorizeDataCache.cs new file mode 100644 index 0000000000..92cdf1fb39 --- /dev/null +++ b/src/Components/Components/src/Auth/AttributeAuthorizeDataCache.cs @@ -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 _cache + = new ConcurrentDictionary(); + + 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().Any()) + { + return null; + } + + var authorizeDataAttributes = allAttributes.OfType().ToArray(); + return authorizeDataAttributes.Length > 0 ? authorizeDataAttributes : null; + } + } +} diff --git a/src/Components/Components/src/Auth/AuthorizeView.cs b/src/Components/Components/src/Auth/AuthorizeView.cs new file mode 100644 index 0000000000..f1a65b0804 --- /dev/null +++ b/src/Components/Components/src/Auth/AuthorizeView.cs @@ -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 +{ + /// + /// Displays differing content depending on the user's authorization status. + /// + public class AuthorizeView : AuthorizeViewCore + { + private readonly IAuthorizeData[] selfAsAuthorizeData; + + /// + /// Constructs an instance of . + /// + public AuthorizeView() + { + selfAsAuthorizeData = new[] { new AuthorizeDataAdapter(this) }; + } + + /// + /// The policy name that determines whether the content can be displayed. + /// + [Parameter] public string Policy { get; private set; } + + /// + /// A comma delimited list of roles that are allowed to display the content. + /// + [Parameter] public string Roles { get; private set; } + + /// + /// Gets the data used for authorization. + /// + protected override IAuthorizeData[] GetAuthorizeData() + => selfAsAuthorizeData; + } +} diff --git a/src/Components/Components/src/Auth/AuthorizeView.razor b/src/Components/Components/src/Auth/AuthorizeView.razor deleted file mode 100644 index 0514ba3f9c..0000000000 --- a/src/Components/Components/src/Auth/AuthorizeView.razor +++ /dev/null @@ -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 { get; set; } - - /// - /// The content that will be displayed if the user is authorized. - /// - [Parameter] public RenderFragment ChildContent { get; private set; } - - /// - /// The content that will be displayed if the user is not authorized. - /// - [Parameter] public RenderFragment NotAuthorized { get; private set; } - - /// - /// 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 . - /// - [Parameter] public RenderFragment Authorized { get; private set; } - - /// - /// The content that will be displayed while asynchronous authorization is in progress. - /// - [Parameter] public RenderFragment Authorizing { get; private set; } - - /// - /// The policy name that determines whether the content can be displayed. - /// - [Parameter] public string Policy { get; private set; } - - /// - /// A comma delimited list of roles that are allowed to display the content. - /// - [Parameter] public string Roles { get; private set; } - - /// - /// The resource to which access is being controlled. - /// - [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 IsAuthorizedAsync(ClaimsPrincipal user) - { - var policy = await AuthorizationPolicy.CombineAsync( - AuthorizationPolicyProvider, selfAsAuthorizeData); - var result = await AuthorizationService.AuthorizeAsync(user, Resource, policy); - return result.Succeeded; - } -} diff --git a/src/Components/Components/src/Auth/AuthorizeViewCore.cs b/src/Components/Components/src/Auth/AuthorizeViewCore.cs new file mode 100644 index 0000000000..07e2cd1c48 --- /dev/null +++ b/src/Components/Components/src/Auth/AuthorizeViewCore.cs @@ -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 +{ + /// + /// A base class for components that display differing content depending on the user's authorization status. + /// + public abstract class AuthorizeViewCore : ComponentBase + { + private AuthenticationState currentAuthenticationState; + private bool isAuthorized; + + /// + /// The content that will be displayed if the user is authorized. + /// + [Parameter] public RenderFragment ChildContent { get; private set; } + + /// + /// The content that will be displayed if the user is not authorized. + /// + [Parameter] public RenderFragment NotAuthorized { get; private set; } + + /// + /// 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 . + /// + [Parameter] public RenderFragment Authorized { get; private set; } + + /// + /// The content that will be displayed while asynchronous authorization is in progress. + /// + [Parameter] public RenderFragment Authorizing { get; private set; } + + /// + /// The resource to which access is being controlled. + /// + [Parameter] public object Resource { get; private set; } + + [CascadingParameter] private Task AuthenticationState { get; set; } + + [Inject] private IAuthorizationPolicyProvider AuthorizationPolicyProvider { get; set; } + + [Inject] private IAuthorizationService AuthorizationService { get; set; } + + /// + 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)); + } + } + + /// + 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); + } + + /// + /// Gets the data required to apply authorization rules. + /// + protected abstract IAuthorizeData[] GetAuthorizeData(); + + private async Task IsAuthorizedAsync(ClaimsPrincipal user) + { + var authorizeData = GetAuthorizeData(); + var policy = await AuthorizationPolicy.CombineAsync( + AuthorizationPolicyProvider, authorizeData); + var result = await AuthorizationService.AuthorizeAsync(user, Resource, policy); + return result.Succeeded; + } + } +} diff --git a/src/Components/Components/src/Layouts/LayoutDisplay.cs b/src/Components/Components/src/Layouts/LayoutDisplay.cs deleted file mode 100644 index c4ae14ab46..0000000000 --- a/src/Components/Components/src/Layouts/LayoutDisplay.cs +++ /dev/null @@ -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 -{ - /// - /// Displays the specified page component, rendering it inside its layout - /// and any further nested layouts. - /// - public class LayoutDisplay : IComponent - { - internal const string NameOfPage = nameof(Page); - internal const string NameOfPageParameters = nameof(PageParameters); - - private RenderHandle _renderHandle; - - /// - /// Gets or sets the type of the page component to display. - /// The type must implement . - /// - [Parameter] - public Type Page { get; private set; } - - /// - /// Gets or sets the parameters to pass to the page. - /// - [Parameter] - public IDictionary PageParameters { get; private set; } - - /// - public void Configure(RenderHandle renderHandle) - { - _renderHandle = renderHandle; - } - - /// - 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()?.LayoutType; - } -} diff --git a/src/Components/Components/src/Layouts/PageDisplay.cs b/src/Components/Components/src/Layouts/PageDisplay.cs new file mode 100644 index 0000000000..a73a7ee6e2 --- /dev/null +++ b/src/Components/Components/src/Layouts/PageDisplay.cs @@ -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 +{ + /// + /// Displays the specified page component, rendering it inside its layout + /// and any further nested layouts, plus applying any authorization rules. + /// + public class PageDisplay : IComponent + { + private RenderHandle _renderHandle; + + /// + /// Gets or sets the type of the page component to display. + /// The type must implement . + /// + [Parameter] + public Type Page { get; private set; } + + /// + /// Gets or sets the parameters to pass to the page. + /// + [Parameter] + public IDictionary PageParameters { get; private set; } + + /// + /// The content that will be displayed if the user is not authorized. + /// + [Parameter] + public RenderFragment NotAuthorizedContent { get; private set; } + + /// + /// The content that will be displayed while asynchronous authorization is in progress. + /// + [Parameter] + public RenderFragment AuthorizingContent { get; private set; } + + /// + public void Configure(RenderHandle renderHandle) + { + _renderHandle = renderHandle; + } + + /// + 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 authorizedContent = context => pageFragment; + return builder => + { + builder.OpenComponent(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()?.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"); + } +} diff --git a/src/Components/Components/src/Routing/Router.cs b/src/Components/Components/src/Routing/Router.cs index 88c781bbc7..e8060708f4 100644 --- a/src/Components/Components/src/Routing/Router.cs +++ b/src/Components/Components/src/Routing/Router.cs @@ -40,6 +40,16 @@ namespace Microsoft.AspNetCore.Components.Routing /// [Parameter] public RenderFragment NotFoundContent { get; private set; } + /// + /// The content that will be displayed if the user is not authorized. + /// + [Parameter] public RenderFragment NotAuthorizedContent { get; private set; } + + /// + /// The content that will be displayed while asynchronous authorization is in progress. + /// + [Parameter] public RenderFragment AuthorizingContent { get; private set; } + private RouteTable Routes { get; set; } /// @@ -78,9 +88,11 @@ namespace Microsoft.AspNetCore.Components.Routing /// protected virtual void Render(RenderTreeBuilder builder, Type handler, IDictionary 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(); } diff --git a/src/Components/Components/test/Auth/AuthorizeViewTest.cs b/src/Components/Components/test/Auth/AuthorizeViewTest.cs index 60c3f445d6..36a83cd935 100644 --- a/src/Components/Components/test/Auth/AuthorizeViewTest.cs +++ b/src/Components/Components/test/Auth/AuthorizeViewTest.cs @@ -248,7 +248,7 @@ namespace Microsoft.AspNetCore.Components renderer.AssignRootComponentId(rootComponent); var ex = Assert.Throws(() => 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] diff --git a/src/Components/Components/test/LayoutTest.cs b/src/Components/Components/test/PageDisplayTest.cs similarity index 86% rename from src/Components/Components/test/LayoutTest.cs rename to src/Components/Components/test/PageDisplayTest.cs index 9439068bd8..f2b64a7001 100644 --- a/src/Components/Components/test/LayoutTest.cs +++ b/src/Components/Components/test/PageDisplayTest.cs @@ -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 + _renderer.Invoke(() => _pageDisplayComponent.SetParametersAsync(ParameterCollection.FromDictionary(new Dictionary { - { 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 + _renderer.Invoke(() => _pageDisplayComponent.SetParametersAsync(ParameterCollection.FromDictionary(new Dictionary { - { 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 + _renderer.Invoke(() => _pageDisplayComponent.SetParametersAsync(ParameterCollection.FromDictionary(new Dictionary { - { LayoutDisplay.NameOfPage, typeof(ComponentWithLayout) } + { nameof(PageDisplay.Page), typeof(ComponentWithLayout) } }))); // Act - _renderer.Invoke(() => _layoutDisplayComponent.SetParametersAsync(ParameterCollection.FromDictionary(new Dictionary + _renderer.Invoke(() => _pageDisplayComponent.SetParametersAsync(ParameterCollection.FromDictionary(new Dictionary { - { 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 + _renderer.Invoke(() => _pageDisplayComponent.SetParametersAsync(ParameterCollection.FromDictionary(new Dictionary { - { LayoutDisplay.NameOfPage, typeof(ComponentWithLayout) } + { nameof(PageDisplay.Page), typeof(ComponentWithLayout) } }))); // Act - _renderer.Invoke(() => _layoutDisplayComponent.SetParametersAsync(ParameterCollection.FromDictionary(new Dictionary + _renderer.Invoke(() => _pageDisplayComponent.SetParametersAsync(ParameterCollection.FromDictionary(new Dictionary { - { LayoutDisplay.NameOfPage, typeof(ComponentWithNestedLayout) } + { nameof(PageDisplay.Page), typeof(ComponentWithNestedLayout) } }))); // Assert diff --git a/src/Components/test/E2ETest/Tests/AuthTest.cs b/src/Components/test/E2ETest/Tests/AuthTest.cs index 3e9e6ef478..9c2c26b0c6 100644 --- a/src/Components/test/E2ETest/Tests/AuthTest.cs +++ b/src/Components/test/E2ETest/Tests/AuthTest.cs @@ -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); diff --git a/src/Components/test/testassets/BasicTestApp/AuthTest/AuthRouter.razor b/src/Components/test/testassets/BasicTestApp/AuthTest/AuthRouter.razor index 299a5beb87..22951925af 100644 --- a/src/Components/test/testassets/BasicTestApp/AuthTest/AuthRouter.razor +++ b/src/Components/test/testassets/BasicTestApp/AuthTest/AuthRouter.razor @@ -9,7 +9,14 @@ *@ - + + Authorizing... + +
+ Sorry, @(context.User.Identity.Name ?? "anonymous"), you're not authorized. +
+
+

diff --git a/src/Components/test/testassets/BasicTestApp/AuthTest/Links.razor b/src/Components/test/testassets/BasicTestApp/AuthTest/Links.razor index 70f524a763..35ff333ed7 100644 --- a/src/Components/test/testassets/BasicTestApp/AuthTest/Links.razor +++ b/src/Components/test/testassets/BasicTestApp/AuthTest/Links.razor @@ -1,8 +1,11 @@ -@using Microsoft.AspNetCore.Components.Routing

To change the underlying authentication state, go here.

diff --git a/src/Components/test/testassets/BasicTestApp/AuthTest/PageAllowingAnonymous.razor b/src/Components/test/testassets/BasicTestApp/AuthTest/PageAllowingAnonymous.razor new file mode 100644 index 0000000000..7a5d7597e3 --- /dev/null +++ b/src/Components/test/testassets/BasicTestApp/AuthTest/PageAllowingAnonymous.razor @@ -0,0 +1,5 @@ +@page "/PageAllowingAnonymous" +@using Microsoft.AspNetCore.Authorization +@attribute [Authorize(Roles = "NobodyIsInThisRole")] +@attribute [AllowAnonymous] +
Welcome to PageAllowingAnonymous!
diff --git a/src/Components/test/testassets/BasicTestApp/AuthTest/PageRequiringAuthorization.razor b/src/Components/test/testassets/BasicTestApp/AuthTest/PageRequiringAuthorization.razor new file mode 100644 index 0000000000..a509e07df8 --- /dev/null +++ b/src/Components/test/testassets/BasicTestApp/AuthTest/PageRequiringAuthorization.razor @@ -0,0 +1,4 @@ +@page "/PageRequiringAuthorization" +@using Microsoft.AspNetCore.Authorization +@attribute [Authorize] +
Welcome to PageRequiringAuthorization!
diff --git a/src/Components/test/testassets/BasicTestApp/AuthTest/PageRequiringPolicy.razor b/src/Components/test/testassets/BasicTestApp/AuthTest/PageRequiringPolicy.razor new file mode 100644 index 0000000000..61d831b9f6 --- /dev/null +++ b/src/Components/test/testassets/BasicTestApp/AuthTest/PageRequiringPolicy.razor @@ -0,0 +1,4 @@ +@page "/PageRequiringPolicy" +@using Microsoft.AspNetCore.Authorization +@attribute [Authorize(Policy = "NameMustStartWithB")] +
Welcome to PageRequiringPolicy!
diff --git a/src/Components/test/testassets/BasicTestApp/AuthTest/PageRequiringRole.razor b/src/Components/test/testassets/BasicTestApp/AuthTest/PageRequiringRole.razor new file mode 100644 index 0000000000..e12462c7e2 --- /dev/null +++ b/src/Components/test/testassets/BasicTestApp/AuthTest/PageRequiringRole.razor @@ -0,0 +1,4 @@ +@page "/PageRequiringRole" +@using Microsoft.AspNetCore.Authorization +@attribute [Authorize(Roles = "TestRole")] +
Welcome to PageRequiringRole!