Skip to content

Integrate authorization into Blazor router #10491

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
May 28, 2019
3 changes: 2 additions & 1 deletion eng/GenAPI.exclusions.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -49,24 +49,31 @@ public readonly partial struct RenderTreeFrame
// 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; } }
[Microsoft.AspNetCore.Components.ParameterAttribute]
public Microsoft.AspNetCore.Components.RenderFragment<Microsoft.AspNetCore.Components.AuthenticationState> ChildContent { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } private set { throw null; } }
[Microsoft.AspNetCore.Components.ParameterAttribute]
public Microsoft.AspNetCore.Components.RenderFragment<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; }
}
Expand Down Expand Up @@ -218,9 +225,13 @@ void System.IDisposable.Dispose() { }

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]
Expand Down
42 changes: 42 additions & 0 deletions src/Components/Components/src/Auth/AttributeAuthorizeDataCache.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
39 changes: 39 additions & 0 deletions src/Components/Components/src/Auth/AuthorizeView.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
99 changes: 0 additions & 99 deletions src/Components/Components/src/Auth/AuthorizeView.razor

This file was deleted.

111 changes: 111 additions & 0 deletions src/Components/Components/src/Auth/AuthorizeViewCore.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
Loading