Skip to content

Commit 43c2392

Browse files
michaelstaibAniruddh25
authored andcommitted
Hot Chocolate 15 Migration (#2348)
With Hot Chocolate 15 we have invested a lot into security, performance and GraphQL protocol standards. This PR will modernize the GraphQL stack of dab. - [X] Existing Regression tests --------- Co-authored-by: Aniruddh Munde <[email protected]>
1 parent 9befe42 commit 43c2392

File tree

76 files changed

+816
-780
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

76 files changed

+816
-780
lines changed

src/Auth/IAuthorizationResolver.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,8 +115,8 @@ public interface IAuthorizationResolver
115115
/// Returns a list of roles which define permissions for the provided operation.
116116
/// i.e. list of roles which allow the operation 'Read' on entityName.
117117
/// </summary>
118-
/// <param name="entityName">Entity to lookup permissions</param>
119-
/// <param name="operation">Operation to lookup applicable roles</param>
118+
/// <param name="entityName">Entity to lookup permissions.</param>
119+
/// <param name="operation">Operation to lookup applicable roles.</param>
120120
/// <returns>Collection of roles. Empty list if entityPermissionsMap is null.</returns>
121121
public static IEnumerable<string> GetRolesForOperation(
122122
string entityName,

src/Cli.Tests/ModuleInitializer.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,8 @@ public static void Init()
9999
VerifierSettings.IgnoreMember<HostOptions>(options => options.UserProvidedMaxResponseSizeMB);
100100
// Ignore UserProvidedDepthLimit as that's not serialized in our config file.
101101
VerifierSettings.IgnoreMember<GraphQLRuntimeOptions>(options => options.UserProvidedDepthLimit);
102+
// Ignore EnableLegacyDateTimeScalar as that's not serialized in our config file.
103+
VerifierSettings.IgnoreMember<GraphQLRuntimeOptions>(options => options.EnableLegacyDateTimeScalar);
102104
// Customise the path where we store snapshots, so they are easier to locate in a PR review.
103105
VerifyBase.DerivePathInfo(
104106
(sourceFile, projectDirectory, type, method) => new(

src/Cli/Cli.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
<ItemGroup>
3636
<PackageReference Include="CommandLineParser" />
3737
<PackageReference Include="System.IO.Abstractions" />
38+
<PackageReference Include="HotChocolate.Utilities.Introspection" />
3839
</ItemGroup>
3940

4041
<ItemGroup>

src/Cli/Exporter.cs

Lines changed: 36 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
namespace Cli
1717
{
1818
/// <summary>
19-
/// Provides functionality for exporting GraphQL schemas, either by generating from a Azure Cosmos DB database or fetching from a GraphQL API.
19+
/// Provides functionality for exporting GraphQL schemas, either by generating from an Azure Cosmos DB database or fetching from a GraphQL API.
2020
/// </summary>
2121
internal class Exporter
2222
{
@@ -44,10 +44,7 @@ public static bool Export(ExportOptions options, ILogger logger, FileSystemRunti
4444
}
4545

4646
// Load the runtime configuration from the file
47-
if (!loader.TryLoadConfig(
48-
runtimeConfigFile,
49-
out RuntimeConfig? runtimeConfig,
50-
replaceEnvVar: true) || runtimeConfig is null)
47+
if (!loader.TryLoadConfig(runtimeConfigFile, out RuntimeConfig? runtimeConfig, replaceEnvVar: true))
5148
{
5249
logger.LogError("Failed to read the config file: {0}.", runtimeConfigFile);
5350
return false;
@@ -90,14 +87,20 @@ public static bool Export(ExportOptions options, ILogger logger, FileSystemRunti
9087
}
9188

9289
/// <summary>
93-
/// Exports the GraphQL schema either by generating it from a Azure Cosmos DB database or fetching it from a GraphQL API.
90+
/// Exports the GraphQL schema either by generating it from an Azure Cosmos DB database or fetching it from a GraphQL API.
9491
/// </summary>
9592
/// <param name="options">The options for exporting, including sampling mode and schema file name.</param>
9693
/// <param name="runtimeConfig">The runtime configuration for the export process.</param>
9794
/// <param name="fileSystem">The file system abstraction for handling file operations.</param>
95+
/// <param name="loader">The loader for runtime configuration files.</param>
9896
/// <param name="logger">The logger instance for logging information and errors.</param>
9997
/// <returns>A task representing the asynchronous operation.</returns>
100-
private static async Task ExportGraphQL(ExportOptions options, RuntimeConfig runtimeConfig, System.IO.Abstractions.IFileSystem fileSystem, FileSystemRuntimeConfigLoader loader, ILogger logger)
98+
private static async Task ExportGraphQL(
99+
ExportOptions options,
100+
RuntimeConfig runtimeConfig,
101+
IFileSystem fileSystem,
102+
FileSystemRuntimeConfigLoader loader,
103+
ILogger logger)
101104
{
102105
string schemaText;
103106
if (options.Generate)
@@ -146,12 +149,12 @@ internal string ExportGraphQLFromDabService(RuntimeConfig runtimeConfig, ILogger
146149
try
147150
{
148151
logger.LogInformation("Trying to fetch schema from DAB Service using HTTPS endpoint.");
149-
schemaText = GetGraphQLSchema(runtimeConfig, useFallbackURL: false);
152+
schemaText = GetGraphQLSchema(runtimeConfig, useFallbackUrl: false);
150153
}
151154
catch
152155
{
153156
logger.LogInformation("Failed to fetch schema from DAB Service using HTTPS endpoint. Trying with HTTP endpoint.");
154-
schemaText = GetGraphQLSchema(runtimeConfig, useFallbackURL: true);
157+
schemaText = GetGraphQLSchema(runtimeConfig, useFallbackUrl: true);
155158
}
156159

157160
return schemaText;
@@ -161,36 +164,38 @@ internal string ExportGraphQLFromDabService(RuntimeConfig runtimeConfig, ILogger
161164
/// Retrieves the GraphQL schema from the DAB service using either the HTTPS or HTTP endpoint based on the specified fallback option.
162165
/// </summary>
163166
/// <param name="runtimeConfig">The runtime configuration containing the GraphQL path and other settings.</param>
164-
/// <param name="useFallbackURL">A boolean flag indicating whether to use the fallback HTTP endpoint. If false, the method attempts to use the HTTPS endpoint.</param>
165-
internal virtual string GetGraphQLSchema(RuntimeConfig runtimeConfig, bool useFallbackURL = false)
167+
/// <param name="useFallbackUrl">A boolean flag indicating whether to use the fallback HTTP endpoint. If false, the method attempts to use the HTTPS endpoint.</param>
168+
internal virtual string GetGraphQLSchema(RuntimeConfig runtimeConfig, bool useFallbackUrl = false)
166169
{
167-
HttpClient client;
168-
if (!useFallbackURL)
169-
{
170-
client = new( // CodeQL[SM02185] Loading internal server connection
171-
new HttpClientHandler { ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator }
172-
)
173-
{
174-
BaseAddress = new Uri($"https://localhost:5001{runtimeConfig.GraphQLPath}")
175-
};
176-
}
177-
else
178-
{
179-
client = new()
180-
{
181-
BaseAddress = new Uri($"http://localhost:5000{runtimeConfig.GraphQLPath}")
182-
};
183-
}
170+
HttpClient client = CreateIntrospectionClient(runtimeConfig.GraphQLPath, useFallbackUrl);
184171

185-
IntrospectionClient introspectionClient = new();
186-
Task<HotChocolate.Language.DocumentNode> response = introspectionClient.DownloadSchemaAsync(client);
172+
Task<HotChocolate.Language.DocumentNode> response = IntrospectionClient.IntrospectServerAsync(client);
187173
response.Wait();
188174

189175
HotChocolate.Language.DocumentNode node = response.Result;
190176

191177
return node.ToString();
192178
}
193179

180+
private static HttpClient CreateIntrospectionClient(string path, bool useFallbackUrl)
181+
{
182+
if (useFallbackUrl)
183+
{
184+
return new HttpClient { BaseAddress = new Uri($"http://localhost:5000{path}") };
185+
}
186+
187+
// CodeQL[SM02185] Loading internal server connection
188+
return new HttpClient(
189+
new HttpClientHandler
190+
{
191+
ServerCertificateCustomValidationCallback =
192+
HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
193+
})
194+
{
195+
BaseAddress = new Uri($"https://localhost:5001{path}")
196+
};
197+
}
198+
194199
private static async Task<string> ExportGraphQLFromCosmosDB(ExportOptions options, RuntimeConfig runtimeConfig, ILogger logger)
195200
{
196201
// Generate the schema from Azure Cosmos DB database
@@ -219,6 +224,7 @@ private static async Task<string> ExportGraphQLFromCosmosDB(ExportOptions option
219224
/// <param name="options">The options containing the output directory and schema file name.</param>
220225
/// <param name="fileSystem">The file system abstraction for handling file operations.</param>
221226
/// <param name="content">The schema content to be written to the file.</param>
227+
/// <param name="logger">The logger instance for logging information and errors.</param>
222228
private static void WriteSchemaFile(ExportOptions options, IFileSystem fileSystem, string content, ILogger logger)
223229
{
224230

src/Config/FileSystemRuntimeConfigLoader.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ private void OnNewFileContentsDetected(object? sender, EventArgs e)
189189
/// <returns>True if the config was loaded, otherwise false.</returns>
190190
public bool TryLoadConfig(
191191
string path,
192-
[NotNullWhen(true)] out RuntimeConfig? outConfig,
192+
[NotNullWhen(true)] out RuntimeConfig? config,
193193
bool replaceEnvVar = false,
194194
ILogger? logger = null,
195195
bool? isDevMode = null)
@@ -238,7 +238,7 @@ public bool TryLoadConfig(
238238
// mode in the new RuntimeConfig since we do not support hot-reload of the mode.
239239
if (isDevMode is not null && RuntimeConfig.Runtime is not null && RuntimeConfig.Runtime.Host is not null)
240240
{
241-
// Log error when the mode is changed during hot-reload.
241+
// Log error when the mode is changed during hot-reload.
242242
if (isDevMode != this.RuntimeConfig.IsDevelopmentMode())
243243
{
244244
if (logger is null)
@@ -254,7 +254,7 @@ public bool TryLoadConfig(
254254
RuntimeConfig.Runtime.Host.Mode = (bool)isDevMode ? HostMode.Development : HostMode.Production;
255255
}
256256

257-
outConfig = RuntimeConfig;
257+
config = RuntimeConfig;
258258

259259
if (LastValidRuntimeConfig is null)
260260
{
@@ -269,7 +269,7 @@ public bool TryLoadConfig(
269269
RuntimeConfig = LastValidRuntimeConfig;
270270
}
271271

272-
outConfig = null;
272+
config = null;
273273
return false;
274274
}
275275

@@ -284,7 +284,7 @@ public bool TryLoadConfig(
284284
logger.LogError(message: errorMessage, path);
285285
}
286286

287-
outConfig = null;
287+
config = null;
288288
return false;
289289
}
290290

src/Config/ObjectModel/EntityGraphQLOptions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,5 @@ namespace Azure.DataApiBuilder.Config.ObjectModel;
1010
/// <param name="Plural">The pluralisation of the entity. If none is provided a pluralisation of the Singular property is used.</param>
1111
/// <param name="Enabled">Indicates if GraphQL is enabled for the entity.</param>
1212
/// <param name="Operation">When the entity maps to a stored procedure, this represents the GraphQL operation to use, otherwise it will be null.</param>
13-
/// <seealso cref="<https://engdic.org/singular-and-plural-noun-rules-definitions-examples/"/>
13+
/// <seealso cref="https://engdic.org/singular-and-plural-noun-rules-definitions-examples"/>
1414
public record EntityGraphQLOptions(string Singular, string Plural, bool Enabled = true, GraphQLOperation? Operation = null);

src/Config/ObjectModel/GraphQLRuntimeOptions.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ public record GraphQLRuntimeOptions(bool Enabled = true,
1212
int? DepthLimit = null,
1313
MultipleMutationOptions? MultipleMutationOptions = null,
1414
bool EnableAggregation = true,
15-
FeatureFlags? FeatureFlags = null)
15+
FeatureFlags? FeatureFlags = null,
16+
bool EnableLegacyDateTimeScalar = true)
1617
{
1718
public const string DEFAULT_PATH = "/graphql";
1819

@@ -27,7 +28,7 @@ public record GraphQLRuntimeOptions(bool Enabled = true,
2728
public bool UserProvidedDepthLimit { get; init; } = false;
2829

2930
/// <summary>
30-
/// Feature flag contains ephemeral flags passed in to init the runtime options
31+
/// Feature flag contains ephemeral flags passed in to init the runtime options
3132
/// </summary>
3233
[JsonIgnore(Condition = JsonIgnoreCondition.Always)]
3334
public FeatureFlags FeatureFlags { get; init; } = FeatureFlags ?? new FeatureFlags();

src/Core/Authorization/GraphQLAuthorizationHandler.cs

Lines changed: 57 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
using System.Diagnostics.CodeAnalysis;
55
using System.Security.Claims;
6-
using HotChocolate.AspNetCore.Authorization;
6+
using HotChocolate.Authorization;
77
using HotChocolate.Resolvers;
88
using Microsoft.AspNetCore.Http;
99
using Microsoft.Extensions.Primitives;
@@ -15,7 +15,7 @@ namespace Azure.DataApiBuilder.Core.Authorization;
1515
/// The changes in this custom handler enable fetching the ClientRoleHeader value defined within requests (value of X-MS-API-ROLE) HTTP Header.
1616
/// Then, using that value to check the header value against the authenticated ClientPrincipal roles.
1717
/// </summary>
18-
public class GraphQLAuthorizationHandler : HotChocolate.AspNetCore.Authorization.IAuthorizationHandler
18+
public class GraphQLAuthorizationHandler : IAuthorizationHandler
1919
{
2020
/// <summary>
2121
/// Authorize access to field based on contents of @authorize directive.
@@ -27,20 +27,24 @@ public class GraphQLAuthorizationHandler : HotChocolate.AspNetCore.Authorization
2727
/// </summary>
2828
/// <param name="context">The current middleware context.</param>
2929
/// <param name="directive">The authorization directive.</param>
30+
/// <param name="cancellationToken">The cancellation token - not used here.</param>
3031
/// <returns>
3132
/// Returns a value indicating if the current session is authorized to
3233
/// access the resolver data.
3334
/// </returns>
34-
public ValueTask<AuthorizeResult> AuthorizeAsync(IMiddlewareContext context, AuthorizeDirective directive)
35+
public ValueTask<AuthorizeResult> AuthorizeAsync(
36+
IMiddlewareContext context,
37+
AuthorizeDirective directive,
38+
CancellationToken cancellationToken = default)
3539
{
36-
if (!IsUserAuthenticated(context))
40+
if (!IsUserAuthenticated(context.ContextData))
3741
{
3842
return new ValueTask<AuthorizeResult>(AuthorizeResult.NotAuthenticated);
3943
}
4044

4145
// Schemas defining authorization policies are not supported, even when roles are defined appropriately.
4246
// Requests will be short circuited and rejected (authorization forbidden).
43-
if (TryGetApiRoleHeader(context, out string? clientRole) && IsInHeaderDesignatedRole(clientRole, directive.Roles))
47+
if (TryGetApiRoleHeader(context.ContextData, out string? clientRole) && IsInHeaderDesignatedRole(clientRole, directive.Roles))
4448
{
4549
if (!string.IsNullOrEmpty(directive.Policy))
4650
{
@@ -53,19 +57,62 @@ public ValueTask<AuthorizeResult> AuthorizeAsync(IMiddlewareContext context, Aut
5357
return new ValueTask<AuthorizeResult>(AuthorizeResult.NotAllowed);
5458
}
5559

60+
/// <summary>
61+
/// Authorize access to field based on contents of @authorize directive.
62+
/// Validates that the requestor is authenticated, and that the
63+
/// clientRoleHeader is present.
64+
/// Role membership is checked
65+
/// and/or (authorize directive may define policy, roles, or both)
66+
/// an authorization policy is evaluated, if present.
67+
/// </summary>
68+
/// <param name="context">The authorization context.</param>
69+
/// <param name="directives">The list of authorize directives.</param>
70+
/// <param name="cancellationToken">The cancellation token.</param>
71+
/// <returns>The authorize result.</returns>
72+
public ValueTask<AuthorizeResult> AuthorizeAsync(
73+
AuthorizationContext context,
74+
IReadOnlyList<AuthorizeDirective> directives,
75+
CancellationToken cancellationToken = default)
76+
{
77+
if (!IsUserAuthenticated(context.ContextData))
78+
{
79+
return new ValueTask<AuthorizeResult>(AuthorizeResult.NotAuthenticated);
80+
}
81+
82+
foreach (AuthorizeDirective directive in directives)
83+
{
84+
// Schemas defining authorization policies are not supported, even when roles are defined appropriately.
85+
// Requests will be short circuited and rejected (authorization forbidden).
86+
if (TryGetApiRoleHeader(context.ContextData, out string? clientRole) && IsInHeaderDesignatedRole(clientRole, directive.Roles))
87+
{
88+
if (!string.IsNullOrEmpty(directive.Policy))
89+
{
90+
return new ValueTask<AuthorizeResult>(AuthorizeResult.NotAllowed);
91+
}
92+
93+
// directive is satisfied, continue to next directive.
94+
continue;
95+
}
96+
97+
return new ValueTask<AuthorizeResult>(AuthorizeResult.NotAllowed);
98+
}
99+
100+
return new ValueTask<AuthorizeResult>(AuthorizeResult.Allowed);
101+
}
102+
56103
/// <summary>
57104
/// Get the value of the CLIENT_ROLE_HEADER HTTP Header from the HttpContext.
58105
/// HttpContext will be present in IMiddlewareContext.ContextData
59106
/// when HotChocolate is configured to use HttpRequestInterceptor
60107
/// </summary>
61-
/// <param name="context">HotChocolate Middleware Context</param>
108+
/// <param name="contextData">HotChocolate Middleware Context data.</param>
62109
/// <param name="clientRole">Value of the client role header.</param>
63110
/// <seealso cref="https://chillicream.com/docs/hotchocolate/v12/server/interceptors#ihttprequestinterceptor"/>
64111
/// <returns>True, if clientRoleHeader is resolved and clientRole value
65112
/// False, if clientRoleHeader is not resolved, null clientRole value</returns>
66-
private static bool TryGetApiRoleHeader(IMiddlewareContext context, [NotNullWhen(true)] out string? clientRole)
113+
private static bool TryGetApiRoleHeader(IDictionary<string, object?> contextData, [NotNullWhen(true)] out string? clientRole)
67114
{
68-
if (context.ContextData.TryGetValue(nameof(HttpContext), out object? value))
115+
if (contextData.TryGetValue(nameof(HttpContext), out object? value))
69116
{
70117
if (value is not null)
71118
{
@@ -110,9 +157,9 @@ private static bool IsInHeaderDesignatedRole(string clientRoleHeader, IReadOnlyL
110157
/// Returns whether the ClaimsPrincipal in the HotChocolate IMiddlewareContext.ContextData is authenticated.
111158
/// To be authenticated, at least one ClaimsIdentity in ClaimsPrincipal.Identities must be authenticated.
112159
/// </summary>
113-
private static bool IsUserAuthenticated(IMiddlewareContext context)
160+
private static bool IsUserAuthenticated(IDictionary<string, object?> contextData)
114161
{
115-
if (context.ContextData.TryGetValue(nameof(ClaimsPrincipal), out object? claimsPrincipalContextObject)
162+
if (contextData.TryGetValue(nameof(ClaimsPrincipal), out object? claimsPrincipalContextObject)
116163
&& claimsPrincipalContextObject is ClaimsPrincipal principal
117164
&& principal.Identities.Any(claimsIdentity => claimsIdentity.IsAuthenticated))
118165
{
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
namespace HotChocolate;
2+
3+
internal static class DabPathExtensions
4+
{
5+
public static int Depth(this Path path)
6+
=> path.Length - 1;
7+
8+
public static bool IsRootField(this Path path)
9+
=> path.Parent.IsRoot;
10+
}

0 commit comments

Comments
 (0)