Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
c0d1266
Started Migration to Hot Chocolate 14
michaelstaib Aug 23, 2024
ac8b980
Merge branch 'main' into mst/version-14
michaelstaib Dec 9, 2024
02b40e5
Merge branch 'main' into mst/version-14
michaelstaib Dec 16, 2024
6842c64
Use 14.3
michaelstaib Dec 16, 2024
cc214c1
Added more fixes
michaelstaib Dec 16, 2024
4956199
Reworked status code handling
michaelstaib Dec 16, 2024
4910c5d
Lots of small fixes
michaelstaib Dec 16, 2024
acada28
Fixed more compile issues
michaelstaib Dec 17, 2024
d0116de
Fixed more compile issues
michaelstaib Dec 17, 2024
c7df3c3
Put nuget.config back into place
michaelstaib Dec 17, 2024
0eef325
Fixed Formatting issues.
michaelstaib Dec 30, 2024
8c8c496
Removed utils function that was not needed.
michaelstaib Jan 9, 2025
3381e68
Updated HC to 15
michaelstaib Feb 6, 2025
22712b3
Refactord filter type creation
michaelstaib Feb 7, 2025
c0e52d1
Fixes several issues with directives.
michaelstaib Mar 6, 2025
2751e0b
Fixes compile issue MultiSourceQueryExecutionUnitTests
michaelstaib Mar 6, 2025
225771e
Fixed issue where the path was wrongly handled.
michaelstaib Mar 6, 2025
af703c5
Merge branch 'main' into mst/version-14
michaelstaib Mar 6, 2025
ce251b6
Fixed compile errors
michaelstaib Mar 6, 2025
6af755a
Fixed Formatting
michaelstaib Mar 6, 2025
33c3f56
Updated Packages
michaelstaib Mar 6, 2025
234bc71
Fixed issues with metadata key
michaelstaib Mar 7, 2025
25d2e5f
Fixed a couple of comments
michaelstaib Mar 7, 2025
542b926
Fixed a couple of comments
michaelstaib Mar 7, 2025
4367e3a
minor changes
michaelstaib Mar 21, 2025
428dc8b
updated to 15.1 final
michaelstaib Mar 21, 2025
4741cf4
Fixed more issues.
michaelstaib Mar 21, 2025
97cc3b8
Merge branch 'main' into mst/version-14
michaelstaib Mar 21, 2025
9857bfa
Added HotChocolate Telemetry
michaelstaib Mar 21, 2025
f782d35
Merge branch 'main' into mst/version-14
michaelstaib Mar 24, 2025
2f23720
formatting
michaelstaib Mar 24, 2025
6a3a1e0
Merge branch 'main' into mst/version-14
Aniruddh25 Mar 28, 2025
cef4594
Update Nuget.config
Aniruddh25 Mar 28, 2025
3fabe65
Merge main and resolve conflicts
Aniruddh25 May 14, 2025
411a289
Added legacy client handling
michaelstaib May 15, 2025
aa26735
Merge branch 'main' into mst/version-14
michaelstaib May 15, 2025
4728e47
Added review suggestion.
michaelstaib May 15, 2025
b9b342a
Added back nuget config.
michaelstaib May 15, 2025
4ccaa95
Fixed batch auth
michaelstaib May 15, 2025
ce23f99
Merge branch 'main' into mst/version-14
Aniruddh25 May 16, 2025
0d518cc
address comments
Aniruddh25 May 16, 2025
e7c366f
Ignore EnableLegacyDateTimeScalar in snapshot testing
Aniruddh25 May 16, 2025
f09a24d
fix build errors for until we investigate if we need additional Graph…
Aniruddh25 May 16, 2025
766630d
Fix renamed nuget.config
Aniruddh25 May 16, 2025
77d2eb5
Remove unnecessary reference
Aniruddh25 May 16, 2025
34d6752
Fix unit tests
Aniruddh25 May 17, 2025
54dcdfc
Check for IsClientError before setting locations to empty
Aniruddh25 May 19, 2025
3c25bad
Remove unnecessary comment
Aniruddh25 May 19, 2025
76e8fd2
Ignore all the HotReload tests for now due to flakiness
Aniruddh25 May 19, 2025
2700a51
Ignore 3 specific hot reload tests
Aniruddh25 May 19, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/Auth/IAuthorizationResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,8 @@ public interface IAuthorizationResolver
/// Returns a list of roles which define permissions for the provided operation.
/// i.e. list of roles which allow the operation 'Read' on entityName.
/// </summary>
/// <param name="entityName">Entity to lookup permissions</param>
/// <param name="operation">Operation to lookup applicable roles</param>
/// <param name="entityName">Entity to lookup permissions.</param>
/// <param name="operation">Operation to lookup applicable roles.</param>
/// <returns>Collection of roles. Empty list if entityPermissionsMap is null.</returns>
public static IEnumerable<string> GetRolesForOperation(
string entityName,
Expand Down
2 changes: 2 additions & 0 deletions src/Cli.Tests/ModuleInitializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ public static void Init()
VerifierSettings.IgnoreMember<HostOptions>(options => options.UserProvidedMaxResponseSizeMB);
// Ignore UserProvidedDepthLimit as that's not serialized in our config file.
VerifierSettings.IgnoreMember<GraphQLRuntimeOptions>(options => options.UserProvidedDepthLimit);
// Ignore EnableLegacyDateTimeScalar as that's not serialized in our config file.
VerifierSettings.IgnoreMember<GraphQLRuntimeOptions>(options => options.EnableLegacyDateTimeScalar);
// Customise the path where we store snapshots, so they are easier to locate in a PR review.
VerifyBase.DerivePathInfo(
(sourceFile, projectDirectory, type, method) => new(
Expand Down
1 change: 1 addition & 0 deletions src/Cli/Cli.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
<ItemGroup>
<PackageReference Include="CommandLineParser" />
<PackageReference Include="System.IO.Abstractions" />
<PackageReference Include="HotChocolate.Utilities.Introspection" />
</ItemGroup>

<ItemGroup>
Expand Down
66 changes: 36 additions & 30 deletions src/Cli/Exporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
namespace Cli
{
/// <summary>
/// Provides functionality for exporting GraphQL schemas, either by generating from a Azure Cosmos DB database or fetching from a GraphQL API.
/// Provides functionality for exporting GraphQL schemas, either by generating from an Azure Cosmos DB database or fetching from a GraphQL API.
/// </summary>
internal class Exporter
{
Expand Down Expand Up @@ -44,10 +44,7 @@ public static bool Export(ExportOptions options, ILogger logger, FileSystemRunti
}

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

/// <summary>
/// Exports the GraphQL schema either by generating it from a Azure Cosmos DB database or fetching it from a GraphQL API.
/// Exports the GraphQL schema either by generating it from an Azure Cosmos DB database or fetching it from a GraphQL API.
/// </summary>
/// <param name="options">The options for exporting, including sampling mode and schema file name.</param>
/// <param name="runtimeConfig">The runtime configuration for the export process.</param>
/// <param name="fileSystem">The file system abstraction for handling file operations.</param>
/// <param name="loader">The loader for runtime configuration files.</param>
/// <param name="logger">The logger instance for logging information and errors.</param>
/// <returns>A task representing the asynchronous operation.</returns>
private static async Task ExportGraphQL(ExportOptions options, RuntimeConfig runtimeConfig, System.IO.Abstractions.IFileSystem fileSystem, FileSystemRuntimeConfigLoader loader, ILogger logger)
private static async Task ExportGraphQL(
ExportOptions options,
RuntimeConfig runtimeConfig,
IFileSystem fileSystem,
FileSystemRuntimeConfigLoader loader,
ILogger logger)
{
string schemaText;
if (options.Generate)
Expand Down Expand Up @@ -146,12 +149,12 @@ internal string ExportGraphQLFromDabService(RuntimeConfig runtimeConfig, ILogger
try
{
logger.LogInformation("Trying to fetch schema from DAB Service using HTTPS endpoint.");
schemaText = GetGraphQLSchema(runtimeConfig, useFallbackURL: false);
schemaText = GetGraphQLSchema(runtimeConfig, useFallbackUrl: false);
}
catch
{
logger.LogInformation("Failed to fetch schema from DAB Service using HTTPS endpoint. Trying with HTTP endpoint.");
schemaText = GetGraphQLSchema(runtimeConfig, useFallbackURL: true);
schemaText = GetGraphQLSchema(runtimeConfig, useFallbackUrl: true);
}

return schemaText;
Expand All @@ -161,36 +164,38 @@ internal string ExportGraphQLFromDabService(RuntimeConfig runtimeConfig, ILogger
/// Retrieves the GraphQL schema from the DAB service using either the HTTPS or HTTP endpoint based on the specified fallback option.
/// </summary>
/// <param name="runtimeConfig">The runtime configuration containing the GraphQL path and other settings.</param>
/// <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>
internal virtual string GetGraphQLSchema(RuntimeConfig runtimeConfig, bool useFallbackURL = false)
/// <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>
internal virtual string GetGraphQLSchema(RuntimeConfig runtimeConfig, bool useFallbackUrl = false)
{
HttpClient client;
if (!useFallbackURL)
{
client = new( // CodeQL[SM02185] Loading internal server connection
new HttpClientHandler { ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator }
)
{
BaseAddress = new Uri($"https://localhost:5001{runtimeConfig.GraphQLPath}")
};
}
else
{
client = new()
{
BaseAddress = new Uri($"http://localhost:5000{runtimeConfig.GraphQLPath}")
};
}
HttpClient client = CreateIntrospectionClient(runtimeConfig.GraphQLPath, useFallbackUrl);

IntrospectionClient introspectionClient = new();
Task<HotChocolate.Language.DocumentNode> response = introspectionClient.DownloadSchemaAsync(client);
Task<HotChocolate.Language.DocumentNode> response = IntrospectionClient.IntrospectServerAsync(client);
response.Wait();

HotChocolate.Language.DocumentNode node = response.Result;

return node.ToString();
}

private static HttpClient CreateIntrospectionClient(string path, bool useFallbackUrl)
{
if (useFallbackUrl)
{
return new HttpClient { BaseAddress = new Uri($"http://localhost:5000{path}") };
}

// CodeQL[SM02185] Loading internal server connection
return new HttpClient(
new HttpClientHandler
{
ServerCertificateCustomValidationCallback =
HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
})
{
BaseAddress = new Uri($"https://localhost:5001{path}")
};
}

private static async Task<string> ExportGraphQLFromCosmosDB(ExportOptions options, RuntimeConfig runtimeConfig, ILogger logger)
{
// Generate the schema from Azure Cosmos DB database
Expand Down Expand Up @@ -219,6 +224,7 @@ private static async Task<string> ExportGraphQLFromCosmosDB(ExportOptions option
/// <param name="options">The options containing the output directory and schema file name.</param>
/// <param name="fileSystem">The file system abstraction for handling file operations.</param>
/// <param name="content">The schema content to be written to the file.</param>
/// <param name="logger">The logger instance for logging information and errors.</param>
private static void WriteSchemaFile(ExportOptions options, IFileSystem fileSystem, string content, ILogger logger)
{

Expand Down
10 changes: 5 additions & 5 deletions src/Config/FileSystemRuntimeConfigLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ private void OnNewFileContentsDetected(object? sender, EventArgs e)
/// <returns>True if the config was loaded, otherwise false.</returns>
public bool TryLoadConfig(
string path,
[NotNullWhen(true)] out RuntimeConfig? outConfig,
[NotNullWhen(true)] out RuntimeConfig? config,
bool replaceEnvVar = false,
ILogger? logger = null,
bool? isDevMode = null)
Expand Down Expand Up @@ -238,7 +238,7 @@ public bool TryLoadConfig(
// mode in the new RuntimeConfig since we do not support hot-reload of the mode.
if (isDevMode is not null && RuntimeConfig.Runtime is not null && RuntimeConfig.Runtime.Host is not null)
{
// Log error when the mode is changed during hot-reload.
// Log error when the mode is changed during hot-reload.
if (isDevMode != this.RuntimeConfig.IsDevelopmentMode())
{
if (logger is null)
Expand All @@ -254,7 +254,7 @@ public bool TryLoadConfig(
RuntimeConfig.Runtime.Host.Mode = (bool)isDevMode ? HostMode.Development : HostMode.Production;
}

outConfig = RuntimeConfig;
config = RuntimeConfig;

if (LastValidRuntimeConfig is null)
{
Expand All @@ -269,7 +269,7 @@ public bool TryLoadConfig(
RuntimeConfig = LastValidRuntimeConfig;
}

outConfig = null;
config = null;
return false;
}

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

outConfig = null;
config = null;
return false;
}

Expand Down
2 changes: 1 addition & 1 deletion src/Config/ObjectModel/EntityGraphQLOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,5 @@ namespace Azure.DataApiBuilder.Config.ObjectModel;
/// <param name="Plural">The pluralisation of the entity. If none is provided a pluralisation of the Singular property is used.</param>
/// <param name="Enabled">Indicates if GraphQL is enabled for the entity.</param>
/// <param name="Operation">When the entity maps to a stored procedure, this represents the GraphQL operation to use, otherwise it will be null.</param>
/// <seealso cref="<https://engdic.org/singular-and-plural-noun-rules-definitions-examples/"/>
/// <seealso cref="https://engdic.org/singular-and-plural-noun-rules-definitions-examples"/>
public record EntityGraphQLOptions(string Singular, string Plural, bool Enabled = true, GraphQLOperation? Operation = null);
5 changes: 3 additions & 2 deletions src/Config/ObjectModel/GraphQLRuntimeOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ public record GraphQLRuntimeOptions(bool Enabled = true,
int? DepthLimit = null,
MultipleMutationOptions? MultipleMutationOptions = null,
bool EnableAggregation = true,
FeatureFlags? FeatureFlags = null)
FeatureFlags? FeatureFlags = null,
bool EnableLegacyDateTimeScalar = true)
{
public const string DEFAULT_PATH = "/graphql";

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

/// <summary>
/// Feature flag contains ephemeral flags passed in to init the runtime options
/// Feature flag contains ephemeral flags passed in to init the runtime options
/// </summary>
[JsonIgnore(Condition = JsonIgnoreCondition.Always)]
public FeatureFlags FeatureFlags { get; init; } = FeatureFlags ?? new FeatureFlags();
Expand Down
67 changes: 57 additions & 10 deletions src/Core/Authorization/GraphQLAuthorizationHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

using System.Diagnostics.CodeAnalysis;
using System.Security.Claims;
using HotChocolate.AspNetCore.Authorization;
using HotChocolate.Authorization;
using HotChocolate.Resolvers;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;
Expand All @@ -15,7 +15,7 @@ namespace Azure.DataApiBuilder.Core.Authorization;
/// The changes in this custom handler enable fetching the ClientRoleHeader value defined within requests (value of X-MS-API-ROLE) HTTP Header.
/// Then, using that value to check the header value against the authenticated ClientPrincipal roles.
/// </summary>
public class GraphQLAuthorizationHandler : HotChocolate.AspNetCore.Authorization.IAuthorizationHandler
public class GraphQLAuthorizationHandler : IAuthorizationHandler
{
/// <summary>
/// Authorize access to field based on contents of @authorize directive.
Expand All @@ -27,20 +27,24 @@ public class GraphQLAuthorizationHandler : HotChocolate.AspNetCore.Authorization
/// </summary>
/// <param name="context">The current middleware context.</param>
/// <param name="directive">The authorization directive.</param>
/// <param name="cancellationToken">The cancellation token - not used here.</param>
/// <returns>
/// Returns a value indicating if the current session is authorized to
/// access the resolver data.
/// </returns>
public ValueTask<AuthorizeResult> AuthorizeAsync(IMiddlewareContext context, AuthorizeDirective directive)
public ValueTask<AuthorizeResult> AuthorizeAsync(
IMiddlewareContext context,
AuthorizeDirective directive,
CancellationToken cancellationToken = default)
{
if (!IsUserAuthenticated(context))
if (!IsUserAuthenticated(context.ContextData))
{
return new ValueTask<AuthorizeResult>(AuthorizeResult.NotAuthenticated);
}

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

/// <summary>
/// Authorize access to field based on contents of @authorize directive.
/// Validates that the requestor is authenticated, and that the
/// clientRoleHeader is present.
/// Role membership is checked
/// and/or (authorize directive may define policy, roles, or both)
/// an authorization policy is evaluated, if present.
/// </summary>
/// <param name="context">The authorization context.</param>
/// <param name="directives">The list of authorize directives.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The authorize result.</returns>
public ValueTask<AuthorizeResult> AuthorizeAsync(
AuthorizationContext context,
IReadOnlyList<AuthorizeDirective> directives,
CancellationToken cancellationToken = default)
{
if (!IsUserAuthenticated(context.ContextData))
{
return new ValueTask<AuthorizeResult>(AuthorizeResult.NotAuthenticated);
}

foreach (AuthorizeDirective directive in directives)
{
// Schemas defining authorization policies are not supported, even when roles are defined appropriately.
// Requests will be short circuited and rejected (authorization forbidden).
if (TryGetApiRoleHeader(context.ContextData, out string? clientRole) && IsInHeaderDesignatedRole(clientRole, directive.Roles))
{
if (!string.IsNullOrEmpty(directive.Policy))
{
return new ValueTask<AuthorizeResult>(AuthorizeResult.NotAllowed);
}

// directive is satisfied, continue to next directive.
continue;
}

return new ValueTask<AuthorizeResult>(AuthorizeResult.NotAllowed);
}

return new ValueTask<AuthorizeResult>(AuthorizeResult.Allowed);
}

/// <summary>
/// Get the value of the CLIENT_ROLE_HEADER HTTP Header from the HttpContext.
/// HttpContext will be present in IMiddlewareContext.ContextData
/// when HotChocolate is configured to use HttpRequestInterceptor
/// </summary>
/// <param name="context">HotChocolate Middleware Context</param>
/// <param name="contextData">HotChocolate Middleware Context data.</param>
/// <param name="clientRole">Value of the client role header.</param>
/// <seealso cref="https://chillicream.com/docs/hotchocolate/v12/server/interceptors#ihttprequestinterceptor"/>
/// <returns>True, if clientRoleHeader is resolved and clientRole value
/// False, if clientRoleHeader is not resolved, null clientRole value</returns>
private static bool TryGetApiRoleHeader(IMiddlewareContext context, [NotNullWhen(true)] out string? clientRole)
private static bool TryGetApiRoleHeader(IDictionary<string, object?> contextData, [NotNullWhen(true)] out string? clientRole)
{
if (context.ContextData.TryGetValue(nameof(HttpContext), out object? value))
if (contextData.TryGetValue(nameof(HttpContext), out object? value))
{
if (value is not null)
{
Expand Down Expand Up @@ -110,9 +157,9 @@ private static bool IsInHeaderDesignatedRole(string clientRoleHeader, IReadOnlyL
/// Returns whether the ClaimsPrincipal in the HotChocolate IMiddlewareContext.ContextData is authenticated.
/// To be authenticated, at least one ClaimsIdentity in ClaimsPrincipal.Identities must be authenticated.
/// </summary>
private static bool IsUserAuthenticated(IMiddlewareContext context)
private static bool IsUserAuthenticated(IDictionary<string, object?> contextData)
{
if (context.ContextData.TryGetValue(nameof(ClaimsPrincipal), out object? claimsPrincipalContextObject)
if (contextData.TryGetValue(nameof(ClaimsPrincipal), out object? claimsPrincipalContextObject)
&& claimsPrincipalContextObject is ClaimsPrincipal principal
&& principal.Identities.Any(claimsIdentity => claimsIdentity.IsAuthenticated))
{
Expand Down
10 changes: 10 additions & 0 deletions src/Core/Extensions/DabPathExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace HotChocolate;

internal static class DabPathExtensions
{
public static int Depth(this Path path)
=> path.Length - 1;

public static bool IsRootField(this Path path)
=> path.Parent.IsRoot;
}
Loading
Loading