-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathAuthorization.cs
More file actions
176 lines (143 loc) · 6.04 KB
/
Copy pathAuthorization.cs
File metadata and controls
176 lines (143 loc) · 6.04 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
using System.Security.Claims;
namespace Shiny.Net.HttpServer.Security;
/// <summary>What the caller has to satisfy. Everything a policy contains must pass.</summary>
public interface IAuthorizationRequirement
{
/// <summary>How this requirement reads in a denial reason, e.g. <c>role 'admin'</c>.</summary>
string Describe();
ValueTask<bool> IsSatisfiedAsync(AuthorizationContext context);
}
/// <summary>What a requirement gets to look at.</summary>
public sealed class AuthorizationContext(HttpContext httpContext, ClaimsPrincipal user)
{
public HttpContext HttpContext { get; } = httpContext;
public ClaimsPrincipal User { get; } = user;
}
/// <summary>
/// A named set of requirements. Built through <see cref="AuthorizationPolicyBuilder"/> and looked up
/// by the name an <c>[Authorize(Policy = "...")]</c> asked for.
/// </summary>
public sealed class AuthorizationPolicy(IReadOnlyList<IAuthorizationRequirement> requirements)
{
public IReadOnlyList<IAuthorizationRequirement> Requirements { get; } = requirements;
/// <summary>
/// Evaluates every requirement. Returns null on success, or the first failure's description —
/// which becomes a log line, never a response body: telling an anonymous caller exactly which
/// claim they are missing is a free hint for someone probing.
/// </summary>
public async ValueTask<string?> EvaluateAsync(AuthorizationContext context)
{
foreach (var requirement in this.Requirements)
{
if (!await requirement.IsSatisfiedAsync(context).ConfigureAwait(false))
return requirement.Describe();
}
return null;
}
}
/// <summary>Assembles a policy.</summary>
public sealed class AuthorizationPolicyBuilder
{
readonly List<IAuthorizationRequirement> requirements = [];
/// <summary>Requires an authenticated caller. Implied by every other requirement.</summary>
public AuthorizationPolicyBuilder RequireAuthenticatedUser()
{
this.requirements.Add(new AuthenticatedUserRequirement());
return this;
}
/// <summary>Requires any one of the given roles.</summary>
public AuthorizationPolicyBuilder RequireRole(params string[] roles)
{
ArgumentNullException.ThrowIfNull(roles);
this.requirements.Add(new RoleRequirement(roles));
return this;
}
/// <summary>Requires a claim, optionally with one of the given values.</summary>
public AuthorizationPolicyBuilder RequireClaim(string claimType, params string[] allowedValues)
{
ArgumentException.ThrowIfNullOrEmpty(claimType);
this.requirements.Add(new ClaimRequirement(claimType, allowedValues));
return this;
}
/// <summary>Requires an arbitrary predicate — the escape hatch for anything above.</summary>
public AuthorizationPolicyBuilder RequireAssertion(
Func<AuthorizationContext, ValueTask<bool>> assertion,
string? description = null
)
{
ArgumentNullException.ThrowIfNull(assertion);
this.requirements.Add(new AssertionRequirement(assertion, description));
return this;
}
/// <summary>Synchronous <see cref="RequireAssertion(Func{AuthorizationContext, ValueTask{bool}}, string?)"/>.</summary>
public AuthorizationPolicyBuilder RequireAssertion(
Func<AuthorizationContext, bool> assertion,
string? description = null
)
{
ArgumentNullException.ThrowIfNull(assertion);
return this.RequireAssertion(ctx => new ValueTask<bool>(assertion(ctx)), description);
}
public AuthorizationPolicyBuilder AddRequirement(IAuthorizationRequirement requirement)
{
ArgumentNullException.ThrowIfNull(requirement);
this.requirements.Add(requirement);
return this;
}
public AuthorizationPolicy Build()
{
// A policy with nothing in it would authorize anonymous callers, which is never what
// someone writing AddPolicy meant.
if (this.requirements.Count == 0)
this.RequireAuthenticatedUser();
return new AuthorizationPolicy([.. this.requirements]);
}
}
sealed class AuthenticatedUserRequirement : IAuthorizationRequirement
{
public string Describe() => "an authenticated user";
public ValueTask<bool> IsSatisfiedAsync(AuthorizationContext context)
=> new(context.User.Identity?.IsAuthenticated == true);
}
sealed class RoleRequirement(string[] roles) : IAuthorizationRequirement
{
public string Describe() => roles.Length == 1
? $"role '{roles[0]}'"
: $"one of the roles {string.Join(", ", roles.Select(r => $"'{r}'"))}";
public ValueTask<bool> IsSatisfiedAsync(AuthorizationContext context)
{
if (context.User.Identity?.IsAuthenticated != true)
return new ValueTask<bool>(false);
foreach (var role in roles)
{
if (context.User.IsInRole(role))
return new ValueTask<bool>(true);
}
return new ValueTask<bool>(false);
}
}
sealed class ClaimRequirement(string claimType, string[] allowedValues) : IAuthorizationRequirement
{
public string Describe() => allowedValues.Length == 0
? $"claim '{claimType}'"
: $"claim '{claimType}' with one of {string.Join(", ", allowedValues.Select(v => $"'{v}'"))}";
public ValueTask<bool> IsSatisfiedAsync(AuthorizationContext context)
{
if (context.User.Identity?.IsAuthenticated != true)
return new ValueTask<bool>(false);
foreach (var claim in context.User.FindAll(claimType))
{
if (allowedValues.Length == 0 || allowedValues.Contains(claim.Value, StringComparer.Ordinal))
return new ValueTask<bool>(true);
}
return new ValueTask<bool>(false);
}
}
sealed class AssertionRequirement(
Func<AuthorizationContext, ValueTask<bool>> assertion,
string? description
) : IAuthorizationRequirement
{
public string Describe() => description ?? "a custom authorization rule";
public ValueTask<bool> IsSatisfiedAsync(AuthorizationContext context) => assertion(context);
}