-
Notifications
You must be signed in to change notification settings - Fork 165
Expand file tree
/
Copy pathAuthorizationVisitorBase.Validation.cs
More file actions
214 lines (198 loc) · 9.74 KB
/
Copy pathAuthorizationVisitorBase.Validation.cs
File metadata and controls
214 lines (198 loc) · 9.74 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
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
namespace GraphQL.Server.Transports.AspNetCore;
public partial class AuthorizationVisitorBase
{
/// <summary>
/// Validates authorization rules for the schema.
/// Returns a value indicating if validation was successful.
/// </summary>
public virtual ValueTask<bool> ValidateSchemaAsync(ValidationContext context)
=> ValidateAsync(context.Schema, null, context);
/// <summary>
/// Validate a node that is current within the context.
/// </summary>
private ValueTask<bool> ValidateAsync(IProvideMetadata obj, ASTNode? node, ValidationContext context)
=> ValidateAsync(BuildValidationInfo(node, obj, context));
/// <summary>
/// Initializes a new <see cref="ValidationInfo"/> instance for the specified node.
/// </summary>
/// <param name="node">The specified <see cref="ASTNode"/>.</param>
/// <param name="obj">The <see cref="IGraphType"/>, <see cref="IFieldType"/> or <see cref="QueryArgument"/> which has been matched to the node specified in <paramref name="node"/>.</param>
/// <param name="context">The validation context.</param>
private static ValidationInfo BuildValidationInfo(ASTNode? node, IProvideMetadata obj, ValidationContext context)
{
IFieldType? parentFieldType = null;
IGraphType? parentGraphType = null;
if (node is GraphQLField)
{
if (obj is IGraphType)
{
parentFieldType = context.TypeInfo.GetFieldDef(0);
parentGraphType = context.TypeInfo.GetLastType(1)?.GetNamedType();
}
else if (obj is IFieldType)
{
parentGraphType = context.TypeInfo.GetLastType(1)?.GetNamedType();
}
}
else if (node is GraphQLArgument)
{
parentFieldType = context.TypeInfo.GetFieldDef();
parentGraphType = context.TypeInfo.GetLastType(1)?.GetNamedType();
}
return new(obj, node, parentFieldType, parentGraphType, context);
}
/// <summary>Provides contextual information to the schema, graph, field, or query argument being validated.</summary>
/// <param name="Obj">The schema, graph type, field type, or query argument being validated. May be an interface type if fragments are in use.</param>
/// <param name="Node">Null for a schema validation; otherwise the <see cref="GraphQLOperationDefinition"/>, <see cref="GraphQLField"/>, or <see cref="GraphQLArgument"/> being validated.</param>
/// <param name="Context">The validation context; but <see cref="ValidationContext.TypeInfo"/> may not be applicable for node being validated.</param>
/// <param name="ParentFieldType">For graph types other than operations, the field where this type was referenced; for query arguments, the field to which this argument belongs.</param>
/// <param name="ParentGraphType">For graph types, the graph type for the field where this type was referenced; for field types, the graph type to which this field belongs; for query arguments, the graph type for the field to which this argument belongs.</param>
public readonly record struct ValidationInfo(
IProvideMetadata Obj,
ASTNode? Node,
IFieldType? ParentFieldType,
IGraphType? ParentGraphType,
ValidationContext Context);
// contains cached authorization results
private Dictionary<string, bool>? _roleResults; // contains a dictionary of roles that have been checked
private Dictionary<string, AuthorizationResult>? _policyResults; // contains a dictionary of policies that have been checked
private bool? _userIsAuthorized;
/// <summary>
/// Validates authorization rules for the specified schema, graph, field or query argument.
/// Does not consider <see cref="AuthorizationExtensions.IsAnonymousAllowed(IProvideMetadata)"/>
/// as this is handled elsewhere.
/// Returns a value indicating if validation was successful for this node.
/// </summary>
protected virtual async ValueTask<bool> ValidateAsync(ValidationInfo info)
{
//note: in v7 currently: IsAuthorizationRequired returns true if authorization is required, or if policies or roles are set
if (info.Obj.GetMetadata(AuthorizationExtensions.AUTHORIZE_KEY, false) /* info.Obj.IsAuthorizationRequired() */)
{
var authorized = _userIsAuthorized ??= IsAuthenticated;
if (!authorized)
{
HandleNodeNotAuthorized(info);
return false;
}
}
var policies = info.Obj.GetPolicies();
if (policies?.Count > 0)
{
_policyResults ??= new Dictionary<string, AuthorizationResult>();
foreach (var policy in policies)
{
if (!_policyResults.TryGetValue(policy, out var result))
{
result = await AuthorizeAsync(policy);
_policyResults.Add(policy, result);
}
if (!result.Succeeded)
{
HandleNodeNotInPolicy(info, policy, result);
return false;
}
}
}
var roles = info.Obj.GetRoles();
if (roles?.Count > 0)
{
_roleResults ??= new Dictionary<string, bool>();
foreach (var role in roles)
{
if (!_roleResults.TryGetValue(role, out var result))
{
result = IsInRole(role);
_roleResults.Add(role, result);
}
if (result)
goto PassRoles;
}
HandleNodeNotInRoles(info, roles);
return false;
}
PassRoles:
return true;
}
/// <inheritdoc cref="IIdentity.IsAuthenticated"/>
protected abstract bool IsAuthenticated { get; }
/// <inheritdoc cref="ClaimsPrincipal.IsInRole(string)"/>
protected abstract bool IsInRole(string role);
/// <inheritdoc cref="IAuthorizationService.AuthorizeAsync(ClaimsPrincipal, object, string)"/>
protected abstract ValueTask<AuthorizationResult> AuthorizeAsync(string policy);
/// <summary>
/// Adds a error to the validation context indicating that the user is not authenticated
/// as required by this graph, field or query argument.
/// </summary>
/// <param name="info">Information about the node being validated.</param>
protected virtual void HandleNodeNotAuthorized(ValidationInfo info)
{
var resource = GenerateResourceDescription(info);
var err = info.Node == null ? new AccessDeniedError(resource) : new AccessDeniedError(resource, info.Context.Document.Source, info.Node);
info.Context.ReportError(err);
}
/// <summary>
/// Adds a error to the validation context indicating that the user is not a member of any of
/// the roles required by this graph, field or query argument.
/// </summary>
/// <param name="info">Information about the node being validated.</param>
/// <param name="roles">The list of roles of which the user must be a member.</param>
protected virtual void HandleNodeNotInRoles(ValidationInfo info, List<string> roles)
{
var resource = GenerateResourceDescription(info);
var err = info.Node == null ? new AccessDeniedError(resource) : new AccessDeniedError(resource, info.Context.Document.Source, info.Node);
err.RolesRequired = roles;
info.Context.ReportError(err);
}
/// <summary>
/// Adds a error to the validation context indicating that the user does not meet the
/// authorization policy required by this graph, field or query argument.
/// </summary>
/// <param name="info">Information about the node being validated.</param>
/// <param name="policy">The policy which these nodes are being authenticated against.</param>
/// <param name="authorizationResult">The result of the authentication request.</param>
protected virtual void HandleNodeNotInPolicy(ValidationInfo info, string policy, AuthorizationResult authorizationResult)
{
var resource = GenerateResourceDescription(info);
var err = info.Node == null ? new AccessDeniedError(resource) : new AccessDeniedError(resource, info.Context.Document.Source, info.Node);
err.PolicyRequired = policy;
err.PolicyAuthorizationResult = authorizationResult;
info.Context.ReportError(err);
}
/// <summary>
/// Generates a friendly name for a specified graph, field or query argument.
/// </summary>
protected virtual string GenerateResourceDescription(ValidationInfo info)
{
if (info.Obj is ISchema)
{
return "schema";
}
else if (info.Obj is IGraphType graphType)
{
if (info.Node is GraphQLField)
{
return $"type '{graphType.Name}' for field '{info.ParentFieldType?.Name}' on type '{info.ParentGraphType?.Name}'";
}
else if (info.Node is GraphQLOperationDefinition op)
{
return $"type '{graphType.Name}' for {op.Operation.ToString().ToLower(System.Globalization.CultureInfo.InvariantCulture)} operation{(!string.IsNullOrEmpty(op.Name?.StringValue) ? $" '{op.Name}'" : null)}";
}
else
{
return $"type '{graphType.Name}'";
}
}
else if (info.Obj is IFieldType fieldType)
{
return $"field '{fieldType.Name}' on type '{info.ParentGraphType?.Name}'";
}
else if (info.Obj is QueryArgument queryArgument)
{
return $"argument '{queryArgument.Name}' for field '{info.ParentFieldType?.Name}' on type '{info.ParentGraphType?.Name}'";
}
else
{
return info.Node?.GetType().Name ?? "unknown";
}
}
}