Skip to content

Commit d1e1acd

Browse files
Integrate authorization with Router/PageDisplay
1 parent 0b7be63 commit d1e1acd

File tree

4 files changed

+134
-24
lines changed

4 files changed

+134
-24
lines changed
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Concurrent;
6+
using System.Linq;
7+
using System.Reflection;
8+
using Microsoft.AspNetCore.Authorization;
9+
10+
namespace Microsoft.AspNetCore.Components.Auth
11+
{
12+
internal static class AttributeAuthorizeDataCache
13+
{
14+
private static ConcurrentDictionary<Type, IAuthorizeData[]> _cache
15+
= new ConcurrentDictionary<Type, IAuthorizeData[]>();
16+
17+
public static IAuthorizeData[] GetAuthorizeDataForType(Type type)
18+
{
19+
IAuthorizeData[] result;
20+
if (!_cache.TryGetValue(type, out result))
21+
{
22+
result = ComputeAuthorizeDataForType(type);
23+
_cache[type] = result; // Safe race - doesn't matter if it overwrites
24+
}
25+
26+
return result;
27+
}
28+
29+
private static IAuthorizeData[] ComputeAuthorizeDataForType(Type type)
30+
{
31+
// Allow Anonymous skips all authorization
32+
var allAttributes = type.GetCustomAttributes(inherit: true);
33+
if (allAttributes.OfType<IAllowAnonymous>().Any())
34+
{
35+
return null;
36+
}
37+
38+
var authorizeDataAttributes = allAttributes.OfType<IAuthorizeData>().ToArray();
39+
return authorizeDataAttributes.Length > 0 ? authorizeDataAttributes : null;
40+
}
41+
}
42+
}

src/Components/Components/src/Auth/AuthorizeViewCore.razor

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,11 @@ else
6363
throw new InvalidOperationException($"When using {nameof(AuthorizeView)}, do not specify both '{nameof(Authorized)}' and '{nameof(ChildContent)}'.");
6464
}
6565

66+
if (AuthenticationState == null)
67+
{
68+
throw new InvalidOperationException($"Authorization requires a cascading parameter of type Task<{nameof(AuthenticationState)}>. Consider using {typeof(CascadingAuthenticationState).Name} to supply this.");
69+
}
70+
6671
// First render in pending state
6772
// If the task has already completed, this render will be skipped
6873
currentAuthenticationState = null;

src/Components/Components/src/Layouts/PageDisplay.cs

Lines changed: 73 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,11 @@
55
using System.Collections.Generic;
66
using System.Reflection;
77
using System.Threading.Tasks;
8-
using Microsoft.AspNetCore.Components;
8+
using Microsoft.AspNetCore.Authorization;
9+
using Microsoft.AspNetCore.Components.Auth;
10+
using Microsoft.AspNetCore.Components.Internal;
911
using Microsoft.AspNetCore.Components.Layouts;
12+
using Microsoft.AspNetCore.Components.RenderTree;
1013

1114
namespace Microsoft.AspNetCore.Components
1215
{
@@ -16,9 +19,6 @@ namespace Microsoft.AspNetCore.Components
1619
/// </summary>
1720
public class PageDisplay : IComponent
1821
{
19-
internal const string NameOfPage = nameof(Page);
20-
internal const string NameOfPageParameters = nameof(PageParameters);
21-
2222
private RenderHandle _renderHandle;
2323

2424
/// <summary>
@@ -34,6 +34,18 @@ public class PageDisplay : IComponent
3434
[Parameter]
3535
public IDictionary<string, object> PageParameters { get; private set; }
3636

37+
/// <summary>
38+
/// The content that will be displayed if the user is not authorized.
39+
/// </summary>
40+
[Parameter]
41+
public RenderFragment<AuthenticationState> NotAuthorizedContent { get; private set; }
42+
43+
/// <summary>
44+
/// The content that will be displayed while asynchronous authorization is in progress.
45+
/// </summary>
46+
[Parameter]
47+
public RenderFragment AuthorizingContent { get; private set; }
48+
3749
/// <inheritdoc />
3850
public void Configure(RenderHandle renderHandle)
3951
{
@@ -50,41 +62,80 @@ public Task SetParametersAsync(ParameterCollection parameters)
5062

5163
private void Render()
5264
{
53-
// In the middle, we render the requested page
54-
var fragment = RenderComponentWithBody(Page, bodyParam: null);
65+
// In the middle goes the requested page
66+
var fragment = (RenderFragment)RenderPageWithParameters;
67+
68+
// Around that goes an AuthorizeViewCore
69+
fragment = WrapInAuthorizeViewCore(fragment);
5570

56-
// Repeatedly wrap it in each layer of nested layout until we get
71+
// Then repeatedly wrap that in each layer of nested layout until we get
5772
// to a layout that has no parent
5873
Type layoutType = Page;
5974
while ((layoutType = GetLayoutType(layoutType)) != null)
6075
{
61-
fragment = RenderComponentWithBody(layoutType, fragment);
76+
fragment = WrapInLayout(layoutType, fragment);
6277
}
6378

6479
_renderHandle.Render(fragment);
6580
}
6681

67-
private RenderFragment RenderComponentWithBody(Type componentType, RenderFragment bodyParam) => builder =>
82+
private RenderFragment WrapInLayout(Type layoutType, RenderFragment bodyParam) => builder =>
6883
{
69-
builder.OpenComponent(0, componentType);
70-
if (bodyParam != null)
71-
{
72-
builder.AddAttribute(1, LayoutComponentBase.BodyPropertyName, bodyParam);
73-
}
74-
else
84+
builder.OpenComponent(0, layoutType);
85+
builder.AddAttribute(1, LayoutComponentBase.BodyPropertyName, bodyParam);
86+
builder.CloseComponent();
87+
};
88+
89+
private void RenderPageWithParameters(RenderTreeBuilder builder)
90+
{
91+
builder.OpenComponent(0, Page);
92+
93+
if (PageParameters != null)
7594
{
76-
if (PageParameters != null)
95+
foreach (var kvp in PageParameters)
7796
{
78-
foreach (var kvp in PageParameters)
79-
{
80-
builder.AddAttribute(1, kvp.Key, kvp.Value);
81-
}
97+
builder.AddAttribute(1, kvp.Key, kvp.Value);
8298
}
8399
}
100+
84101
builder.CloseComponent();
85-
};
102+
}
103+
104+
private RenderFragment WrapInAuthorizeViewCore(RenderFragment pageFragment)
105+
{
106+
var authorizeData = AttributeAuthorizeDataCache.GetAuthorizeDataForType(Page);
107+
if (authorizeData == null)
108+
{
109+
// No authorization, so no need to wrap the fragment
110+
return pageFragment;
111+
}
86112

87-
private Type GetLayoutType(Type type)
113+
// Some authorization data exists, so we do need to wrap the fragment
114+
RenderFragment<AuthenticationState> authorizedContent = context => pageFragment;
115+
return builder =>
116+
{
117+
builder.OpenComponent<AuthorizeViewWithSuppliedData>(0);
118+
builder.AddAttribute(1, nameof(AuthorizeViewWithSuppliedData.AuthorizeDataParam), authorizeData);
119+
builder.AddAttribute(2, nameof(AuthorizeViewWithSuppliedData.Authorized), authorizedContent);
120+
builder.AddAttribute(3, nameof(AuthorizeViewWithSuppliedData.NotAuthorized), NotAuthorizedContent ?? DefaultNotAuthorizedContent);
121+
builder.AddAttribute(4, nameof(AuthorizeViewWithSuppliedData.Authorizing), AuthorizingContent);
122+
builder.CloseComponent();
123+
};
124+
}
125+
126+
private static Type GetLayoutType(Type type)
88127
=> type.GetCustomAttribute<LayoutAttribute>()?.LayoutType;
128+
129+
private class AuthorizeViewWithSuppliedData : AuthorizeViewCore
130+
{
131+
[Parameter] public IAuthorizeData[] AuthorizeDataParam { get; private set; }
132+
133+
protected override IAuthorizeData[] AuthorizeData => AuthorizeDataParam;
134+
}
135+
136+
// There has to be some default content. If we render blank by default, developers
137+
// will find it hard to guess why their UI isn't appearing.
138+
private static RenderFragment DefaultNotAuthorizedContent(AuthenticationState authenticationState)
139+
=> builder => builder.AddContent(0, "Not authorized");
89140
}
90141
}

src/Components/Components/src/Routing/Router.cs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,16 @@ public class Router : IComponent, IHandleAfterRender, IDisposable
4040
/// </summary>
4141
[Parameter] public RenderFragment NotFoundContent { get; private set; }
4242

43+
/// <summary>
44+
/// The content that will be displayed if the user is not authorized.
45+
/// </summary>
46+
[Parameter] public RenderFragment<AuthenticationState> NotAuthorizedContent { get; private set; }
47+
48+
/// <summary>
49+
/// The content that will be displayed while asynchronous authorization is in progress.
50+
/// </summary>
51+
[Parameter] public RenderFragment AuthorizingContent { get; private set; }
52+
4353
private RouteTable Routes { get; set; }
4454

4555
/// <inheritdoc />
@@ -79,8 +89,10 @@ private string StringUntilAny(string str, char[] chars)
7989
protected virtual void Render(RenderTreeBuilder builder, Type handler, IDictionary<string, object> parameters)
8090
{
8191
builder.OpenComponent(0, typeof(PageDisplay));
82-
builder.AddAttribute(1, PageDisplay.NameOfPage, handler);
83-
builder.AddAttribute(2, PageDisplay.NameOfPageParameters, parameters);
92+
builder.AddAttribute(1, nameof(PageDisplay.Page), handler);
93+
builder.AddAttribute(2, nameof(PageDisplay.PageParameters), parameters);
94+
builder.AddAttribute(3, nameof(PageDisplay.NotAuthorizedContent), NotAuthorizedContent);
95+
builder.AddAttribute(4, nameof(PageDisplay.AuthorizingContent), AuthorizingContent);
8496
builder.CloseComponent();
8597
}
8698

0 commit comments

Comments
 (0)