From a464a7f26f6e4e9cf4e554c106947ae20db30294 Mon Sep 17 00:00:00 2001 From: Dmitry Stoyanovsky Date: Thu, 27 Feb 2025 20:20:48 +0100 Subject: [PATCH 1/6] fix(opa-support-topic): Fix OPA middleware to comply with OPA V1 - Add support to validate policy before and after request with IMiddlewareContext (access to input arguments) - Add OPA request extensions handler support - Fix unit tests - Other fixes --- src/Directory.Packages.props | 2 +- ...hocolateAuthorizeRequestExecutorBuilder.cs | 36 +++++- .../IOpaService.cs | 10 ++ .../OpaAuthorizationHandler.cs | 49 ++++----- .../OpaAuthorizationHandlerContext.cs | 25 +++++ .../OpaOptions.cs | 36 +++++- .../OpaService.cs | 27 ++--- .../QueryResponse.cs | 20 ++-- .../Request/DefaultQueryRequestFactory.cs | 48 ++++++-- .../Request/IOpaQueryRequestFactory.cs | 13 ++- .../Request/IPAndPort.cs | 21 +++- .../Request/OpaQueryRequest.cs | 104 +++++++++++------- .../Request/OriginalRequest.cs | 67 ++++++----- .../Request/Policy.cs | 21 ++-- .../AuthorizationAttributeTestData.cs | 14 ++- .../AuthorizationTestData.cs | 24 ++-- .../AuthorizationTests.cs | 54 ++++++++- .../Claims.cs | 6 + .../HasAgeDefinedResponse.cs | 4 + ....AspNetCore.Authorization.Opa.Tests.csproj | 10 +- .../OpaProcess/OpaProcess.cs | 36 ++++++ .../Policies/has_age_defined.rego | 18 ++- .../AuthorizationTests.Policy_Authorized.snap | 2 +- ...ests.Policy_Authorized_WithExtensions.snap | 9 ++ ...thorizationTests.Policy_NotAuthorized.snap | 2 +- .../AuthorizationTests.Policy_NotFound.snap | 6 +- 26 files changed, 471 insertions(+), 193 deletions(-) create mode 100644 src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/OpaAuthorizationHandlerContext.cs create mode 100644 src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/OpaProcess/OpaProcess.cs create mode 100644 src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/__snapshots__/AuthorizationTests.Policy_Authorized_WithExtensions.snap diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index a8134af4a9b..5d576380645 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -39,7 +39,6 @@ - @@ -59,6 +58,7 @@ + diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/Extensions/HotChocolateAuthorizeRequestExecutorBuilder.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/Extensions/HotChocolateAuthorizeRequestExecutorBuilder.cs index a802bb5e6a2..7bdd2ecfdc4 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/Extensions/HotChocolateAuthorizeRequestExecutorBuilder.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/Extensions/HotChocolateAuthorizeRequestExecutorBuilder.cs @@ -1,8 +1,8 @@ using HotChocolate.AspNetCore.Authorization; using HotChocolate.Execution.Configuration; -using Microsoft.Extensions.Configuration; using System.Text.Json; using System.Text.Json.Serialization; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Options; namespace Microsoft.Extensions.DependencyInjection; @@ -42,7 +42,7 @@ public static IRequestExecutorBuilder AddOpaAuthorization( var jsonOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }; jsonOptions.Converters.Add( new JsonStringEnumConverter( @@ -55,6 +55,15 @@ public static IRequestExecutorBuilder AddOpaAuthorization( return builder; } + /// + /// Adds result handler to the OPA options. + /// + /// Instance of . + /// The path to the policy. + /// The PDP decision result. + /// + /// Returns the for chaining in more configurations. + /// public static IRequestExecutorBuilder AddOpaResultHandler( this IRequestExecutorBuilder builder, string policyPath, @@ -66,4 +75,27 @@ public static IRequestExecutorBuilder AddOpaResultHandler( (o, _) => o.PolicyResultHandlers.Add(policyPath, parseResult)); return builder; } + + /// + /// Adds OPA query request extensions handler to the OPA options. + /// + /// Instance of . + /// The path to the policy. + /// The handler for the extensions associated with the Policy. + /// + /// + /// Returns the for chaining in more configurations. + /// + public static IRequestExecutorBuilder AddOpaQueryRequestExtensionsHandler( + this IRequestExecutorBuilder builder, + string policyPath, + OpaQueryRequestExtensionsHandler opaQueryRequestExtensionsHandler) + { + builder.Services + .AddOptions() + .Configure( + (o, _) => o.OpaQueryRequestExtensionsHandlers.Add(policyPath, opaQueryRequestExtensionsHandler)); + return builder; + } + } diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/IOpaService.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/IOpaService.cs index 4218692373b..9840c7af544 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/IOpaService.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/IOpaService.cs @@ -1,7 +1,17 @@ namespace HotChocolate.AspNetCore.Authorization; +/// +/// The OPA service interface communicating with OPA server. +/// public interface IOpaService { + /// + /// The method used to query OPA PDP decision based on the request input. + /// + /// The string parameter representing path of the evaluating policy. + /// The instance . + /// Cancellation token. + /// Task QueryAsync( string policyPath, OpaQueryRequest request, diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/OpaAuthorizationHandler.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/OpaAuthorizationHandler.cs index d2b7c17c779..53fd6226256 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/OpaAuthorizationHandler.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/OpaAuthorizationHandler.cs @@ -19,53 +19,46 @@ public OpaAuthorizationHandler( IOpaQueryRequestFactory requestFactory, IOptions options) { - if (options is null) - { - throw new ArgumentNullException(nameof(options)); - } + ArgumentNullException.ThrowIfNull(options); _opaService = opaService ?? throw new ArgumentNullException(nameof(opaService)); _requestFactory = requestFactory ?? throw new ArgumentNullException(nameof(requestFactory)); _options = options.Value; } - /// - /// Authorize current directive using OPA (Open Policy Agent). - /// - /// The current middleware context. - /// The authorization directive. - /// The cancellation token. - /// - /// Returns a value indicating if the current session is authorized to - /// access the resolver data. - /// + /// public async ValueTask AuthorizeAsync( IMiddlewareContext context, AuthorizeDirective directive, - CancellationToken ct) + CancellationToken cancellationToken = default) { - var authorizationContext = new AuthorizationContext( - context.Schema, - context.Services, - context.ContextData, - context.Operation.Document, - context.Operation.Id); - return await AuthorizeAsync(authorizationContext, directive, ct).ConfigureAwait(false); + return await AuthorizeAsync( + new OpaAuthorizationHandlerContext(context), [directive], cancellationToken).ConfigureAwait(false); } + /// public async ValueTask AuthorizeAsync( AuthorizationContext context, IReadOnlyList directives, + CancellationToken cancellationToken = default) + { + return await AuthorizeAsync( + new OpaAuthorizationHandlerContext(context), directives, cancellationToken).ConfigureAwait(false); + } + + private async ValueTask AuthorizeAsync( + OpaAuthorizationHandlerContext authContext, + IReadOnlyList directives, CancellationToken ct) { if (directives.Count == 1) { - return await AuthorizeAsync(context, directives[0], ct).ConfigureAwait(false); + return await AuthorizeAsync(authContext, directives[0], ct).ConfigureAwait(false); } var tasks = Partitioner.Create(directives) .GetPartitions(2) - .Select(partition => ExecuteAsync(context, partition, AuthorizeAsync, ct)) + .Select(partition => ExecuteAsync(authContext, partition, AuthorizeAsync, ct)) .ToArray(); var first = await Task.WhenAny(tasks).ConfigureAwait(false); @@ -89,12 +82,12 @@ public async ValueTask AuthorizeAsync( return AuthorizeResult.Allowed; static async Task ExecuteAsync( - AuthorizationContext context, + OpaAuthorizationHandlerContext context, IEnumerator partition, Authorize authorize, CancellationToken ct) { - while (partition.MoveNext() && partition.Current is not null) + while (partition.MoveNext()) { var directive = partition.Current; var result = await authorize(context, directive, ct).ConfigureAwait(false); @@ -110,7 +103,7 @@ static async Task ExecuteAsync( } private async ValueTask AuthorizeAsync( - AuthorizationContext context, + OpaAuthorizationHandlerContext context, AuthorizeDirective directive, CancellationToken ct) { @@ -122,7 +115,7 @@ private async ValueTask AuthorizeAsync( } private delegate ValueTask Authorize( - AuthorizationContext context, + OpaAuthorizationHandlerContext context, AuthorizeDirective directive, CancellationToken ct); } diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/OpaAuthorizationHandlerContext.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/OpaAuthorizationHandlerContext.cs new file mode 100644 index 00000000000..59811e92efb --- /dev/null +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/OpaAuthorizationHandlerContext.cs @@ -0,0 +1,25 @@ +namespace HotChocolate.AspNetCore.Authorization; + +/// +/// The OPA authorization handler context. +/// +public class OpaAuthorizationHandlerContext +{ + /// + /// The constructor. + /// + /// Either IMiddlewareContext or AuthorizationContext depending on the phase of + /// a rule execution. + /// + public OpaAuthorizationHandlerContext(object resource) + { + ArgumentNullException.ThrowIfNull(resource); + + Resource = resource; + } + + /// + /// The object representing instance of either IMiddlewareContext or AuthorizationContext. + /// + public object Resource { get; } +} diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/OpaOptions.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/OpaOptions.cs index 955487008f6..67cfd8f4c73 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/OpaOptions.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/OpaOptions.cs @@ -5,26 +5,51 @@ namespace HotChocolate.AspNetCore.Authorization; +/// +/// The class representing OPA configuration options. +/// public sealed class OpaOptions { private readonly ConcurrentDictionary _handlerKeysRegexes = new(); + // ReSharper disable once AutoPropertyCanBeMadeGetOnly.Global public Uri BaseAddress { get; set; } = new("http://127.0.0.1:8181/v1/data/"); + // ReSharper disable once AutoPropertyCanBeMadeGetOnly.Global public TimeSpan Timeout { get; set; } = TimeSpan.FromMilliseconds(250); public JsonSerializerOptions JsonSerializerOptions { get; set; } = new(); public Dictionary PolicyResultHandlers { get; } = new(); + public Dictionary OpaQueryRequestExtensionsHandlers { get; } = new(); + + public OpaQueryRequestExtensionsHandler? GetOpaQueryRequestExtensionsHandler(string policyPath) + { + if (OpaQueryRequestExtensionsHandlers.Count == 0) + { + return null; + } + return OpaQueryRequestExtensionsHandlers.TryGetValue(policyPath, out var handler) + ? handler : + FindHandler(policyPath, OpaQueryRequestExtensionsHandlers); + } + public ParseResult GetPolicyResultParser(string policyPath) { if (PolicyResultHandlers.TryGetValue(policyPath, out var handler)) { return handler; } + handler = FindHandler(policyPath, PolicyResultHandlers); + return handler ?? + throw new InvalidOperationException( + $"No result handler found for policy: {policyPath}"); + } - var maybeHandler = PolicyResultHandlers.SingleOrDefault( + private THandler? FindHandler(string policyPath, Dictionary handlers) + { + var maybeHandler = handlers.SingleOrDefault( k => { var regex = _handlerKeysRegexes.GetOrAdd( @@ -33,14 +58,15 @@ public ParseResult GetPolicyResultParser(string policyPath) k.Key, RegexOptions.Compiled | RegexOptions.Singleline | - RegexOptions.CultureInvariant)); + RegexOptions.CultureInvariant, + TimeSpan.FromMilliseconds(500))); return regex.IsMatch(policyPath); }); - return maybeHandler.Value ?? - throw new InvalidOperationException( - $"No result handler found for policy: {policyPath}"); + return maybeHandler.Value; } } public delegate AuthorizeResult ParseResult(OpaQueryResponse response); + +public delegate object? OpaQueryRequestExtensionsHandler(OpaAuthorizationHandlerContext context); diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/OpaService.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/OpaService.cs index 578763b186d..7576f3e1c67 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/OpaService.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/OpaService.cs @@ -11,36 +11,29 @@ internal sealed class OpaService : IOpaService public OpaService(HttpClient httpClient, IOptions options) { - if (options is null) - { - throw new ArgumentNullException(nameof(options)); - } + ArgumentNullException.ThrowIfNull(options); - _client = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); + ArgumentNullException.ThrowIfNull(httpClient); + + _client = httpClient; _options = options.Value; } public async Task QueryAsync( string policyPath, OpaQueryRequest request, - CancellationToken ct) + CancellationToken cancellationToken = default) { - if (policyPath is null) - { - throw new ArgumentNullException(nameof(policyPath)); - } + ArgumentNullException.ThrowIfNull(policyPath); - if (request is null) - { - throw new ArgumentNullException(nameof(request)); - } + ArgumentNullException.ThrowIfNull(request); using var body = JsonContent.Create(request, options: _options.JsonSerializerOptions); - using var response = await _client.PostAsync(policyPath, body, ct).ConfigureAwait(false); + using var response = await _client.PostAsync(policyPath, body, cancellationToken).ConfigureAwait(false); response.EnsureSuccessStatusCode(); - await using var stream = await response.Content.ReadAsStreamAsync(ct).ConfigureAwait(false); - var document = await JsonDocument.ParseAsync(stream, default, ct); + await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); + var document = await JsonDocument.ParseAsync(stream, default, cancellationToken); return new OpaQueryResponse(document); } } diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/QueryResponse.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/QueryResponse.cs index 18f2a00e917..2c770dbf897 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/QueryResponse.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/QueryResponse.cs @@ -2,24 +2,20 @@ namespace HotChocolate.AspNetCore.Authorization; -public sealed class OpaQueryResponse : IDisposable +/// +/// The class representing OPA query response. +/// +public sealed class OpaQueryResponse(JsonDocument document) : IDisposable { - private readonly JsonDocument _document; - private readonly JsonElement _root; - - public OpaQueryResponse(JsonDocument document) - { - _document = document; - _root = document.RootElement; - } + private readonly JsonElement _root = document.RootElement; public Guid? DecisionId - => _root.TryGetProperty("decisionId", out var value) + => _root.TryGetProperty("decision_id", out var value) ? value.GetGuid() : null; public T? GetResult() - => _root.TryGetProperty("decisionId", out var value) + => _root.TryGetProperty("result", out var value) ? value.Deserialize() : default; @@ -28,5 +24,5 @@ public bool IsEmpty _root.EnumerateObject().Any(); public void Dispose() - => _document.Dispose(); + => document.Dispose(); } diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/Request/DefaultQueryRequestFactory.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/Request/DefaultQueryRequestFactory.cs index 77e6b349659..4a781f80bdc 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/Request/DefaultQueryRequestFactory.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/Request/DefaultQueryRequestFactory.cs @@ -1,23 +1,40 @@ using HotChocolate.Authorization; +using HotChocolate.Resolvers; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Options; namespace HotChocolate.AspNetCore.Authorization; -public sealed class DefaultQueryRequestFactory : IOpaQueryRequestFactory +/// +/// Default implementation of . +/// +internal sealed class DefaultQueryRequestFactory : IOpaQueryRequestFactory { - public OpaQueryRequest CreateRequest(AuthorizationContext context, AuthorizeDirective directive) + private readonly OpaOptions _options; + + public DefaultQueryRequestFactory(IOptions options) { - if (context is null) - { - throw new ArgumentNullException(nameof(context)); - } + ArgumentNullException.ThrowIfNull(options); - if (directive is null) - { - throw new ArgumentNullException(nameof(directive)); - } + _options = options.Value; + } - var httpContext = (HttpContext)context.ContextData[nameof(HttpContext)]!; + /// + public OpaQueryRequest CreateRequest(OpaAuthorizationHandlerContext context, AuthorizeDirective directive) + { + ArgumentNullException.ThrowIfNull(context); + + ArgumentNullException.ThrowIfNull(directive); + + var httpContext = + context.Resource switch + { + IMiddlewareContext middlewareContext => + (HttpContext)middlewareContext.ContextData[nameof(HttpContext)]!, + AuthorizationContext authorizationContext => + (HttpContext)authorizationContext.ContextData[nameof(HttpContext)]!, + _ => throw new ArgumentException("Invalid context data.") + }; var connection = httpContext.Connection; var policy = new Policy( @@ -40,6 +57,13 @@ public OpaQueryRequest CreateRequest(AuthorizationContext context, AuthorizeDire connection.LocalIpAddress!.ToString(), connection.LocalPort); - return new OpaQueryRequest(policy, originalRequest, source, destination); + object? extensions = null; + if (directive.Policy is not null && + _options.GetOpaQueryRequestExtensionsHandler(directive.Policy) is { } extensionsHandler) + { + extensions = extensionsHandler(context); + } + + return new OpaQueryRequest(policy, originalRequest, source, destination, extensions); } } diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/Request/IOpaQueryRequestFactory.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/Request/IOpaQueryRequestFactory.cs index 92a35f2c95c..0ed248eb250 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/Request/IOpaQueryRequestFactory.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/Request/IOpaQueryRequestFactory.cs @@ -2,9 +2,20 @@ namespace HotChocolate.AspNetCore.Authorization; +/// +/// The OPA query request factory interface. +/// public interface IOpaQueryRequestFactory { + /// + /// Creates . + /// + /// The OPA authorization handler context. + /// Depending on the query execution phase the context's Resource is different + /// see for details. + /// + /// The OPA authorization directive. See . OpaQueryRequest CreateRequest( - AuthorizationContext context, + OpaAuthorizationHandlerContext context, AuthorizeDirective directive); } diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/Request/IPAndPort.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/Request/IPAndPort.cs index 55cfd1f9611..c8c49529ec3 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/Request/IPAndPort.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/Request/IPAndPort.cs @@ -1,21 +1,34 @@ namespace HotChocolate.AspNetCore.Authorization; // ReSharper disable once InconsistentNaming +/// +/// A structure to store information about IP address and port in OPA query request input. +/// public sealed class IPAndPort { + /// + /// Public constructor. + /// + /// IP address. + /// Port number. + /// Thrown if port values is out of range: [0:65535]. public IPAndPort(string ipAddress, int port = 0) { - if (port <= 0) - { - throw new ArgumentOutOfRangeException(nameof(port)); - } + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(port); + ArgumentOutOfRangeException.ThrowIfGreaterThan(port, (1 << 16) - 1); IPAddress = ipAddress ?? throw new ArgumentNullException(nameof(ipAddress)); Port = port; } // ReSharper disable once InconsistentNaming + /// + /// IP address string. + /// public string IPAddress { get; } + /// + /// Port value. + /// public int Port { get; } } diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/Request/OpaQueryRequest.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/Request/OpaQueryRequest.cs index 579af63a72a..84adb8af140 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/Request/OpaQueryRequest.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/Request/OpaQueryRequest.cs @@ -1,7 +1,22 @@ namespace HotChocolate.AspNetCore.Authorization; +/// +/// A structure representing OPA query request input. +/// public sealed class OpaQueryRequest { + /// + /// The constructor. + /// + /// The instance of . + /// The instance of . + /// Stores information about the original GraphQl request. + /// The instance . + /// Stores information about source address of the original request + /// The instance . + /// Stores information about destination address of the original request. + /// The instance of object that provides extended information for the OPA query request. + /// Usually represented as a dictionary. public OpaQueryRequest( Policy policy, OriginalRequest request, @@ -9,55 +24,66 @@ public OpaQueryRequest( IPAndPort destination, object? extensions = null) { - if (policy is null) - { - throw new ArgumentNullException(nameof(policy)); - } - - if (request is null) - { - throw new ArgumentNullException(nameof(request)); - } - - if (source is null) - { - throw new ArgumentNullException(nameof(source)); - } - - if (destination is null) - { - throw new ArgumentNullException(nameof(destination)); - } + ArgumentNullException.ThrowIfNull(policy); + + ArgumentNullException.ThrowIfNull(request); + + ArgumentNullException.ThrowIfNull(source); + + ArgumentNullException.ThrowIfNull(destination); Input = new OpaQueryRequestInput(policy, request, source, destination, extensions); } + /// + /// The property to get instance of . + /// public OpaQueryRequestInput Input { get; } - public sealed class OpaQueryRequestInput + /// + /// The class representing an input that will be sent to the OPA server. + /// + /// The instance of . + /// The instance of . + /// Stores information about the original GraphQl request. + /// The instance . + /// Stores information about source address of the original request + /// The instance . + /// Stores information about destination address of the original request. + /// The instance of object the provides extended information for the OPA query request. + /// Usually represented as a dictionary. + public sealed class OpaQueryRequestInput( + Policy policy, + OriginalRequest request, + IPAndPort source, + IPAndPort destination, + object? extensions) { - public OpaQueryRequestInput( - Policy policy, - OriginalRequest request, - IPAndPort source, - IPAndPort destination, - object? extensions) - { - Policy = policy; - Request = request; - Source = source; - Destination = destination; - Extensions = extensions; - } - - public Policy Policy { get; } + /// + /// The property to get instance of . + /// + public Policy Policy { get; } = policy; - public OriginalRequest Request { get; } + /// + /// The property to get instance of . + /// + public OriginalRequest Request { get; } = request; - public IPAndPort Source { get; } + /// + /// The property to get instance of representing + /// the original request source IP address and port. + /// + public IPAndPort Source { get; } = source; - public IPAndPort Destination { get; } + /// + /// The property to get instance of representing + /// the original request destination IP address and port. + /// + public IPAndPort Destination { get; } = destination; - public object? Extensions { get; } + /// + /// The property to get instance of object representing OPA query input extension data. + /// + public object? Extensions { get; } = extensions; } } diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/Request/OriginalRequest.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/Request/OriginalRequest.cs index 5d31d0c67e4..f86f96ebf0f 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/Request/OriginalRequest.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/Request/OriginalRequest.cs @@ -3,33 +3,44 @@ namespace HotChocolate.AspNetCore.Authorization; -public sealed class OriginalRequest +/// +/// The class representing the information about the original GraphQl request. +/// +public sealed class OriginalRequest( + IHeaderDictionary headers, + string host, + string method, + string path, + IEnumerable>? query, + string scheme) { - public OriginalRequest( - IHeaderDictionary headers, - string host, - string method, - string path, - IEnumerable>? query, - string scheme) - { - Headers = headers ?? throw new ArgumentNullException(nameof(headers)); - Host = host ?? throw new ArgumentNullException(nameof(host)); - Method = method ?? throw new ArgumentNullException(nameof(method)); - Path = path ?? throw new ArgumentNullException(nameof(path)); - Query = query; - Scheme = scheme ?? throw new ArgumentNullException(nameof(scheme)); - } - - public IHeaderDictionary Headers { get; } - - public string Host { get; } - - public string Method { get; } - - public string Path { get; } - - public IEnumerable>? Query { get; } - - public string Scheme { get; } + /// + /// Original request headers. + /// + public IHeaderDictionary Headers { get; } = headers ?? throw new ArgumentNullException(nameof(headers)); + + /// + /// Information about the host sent request. + /// + public string Host { get; } = host ?? throw new ArgumentNullException(nameof(host)); + + /// + /// The HTTP request method. + /// + public string Method { get; } = method ?? throw new ArgumentNullException(nameof(method)); + + /// + /// Path of the request. + /// + public string Path { get; } = path ?? throw new ArgumentNullException(nameof(path)); + + /// + /// The query of the request. + /// + public IEnumerable>? Query { get; } = query; + + /// + /// GraphQl schema of the request. + /// + public string Scheme { get; } = scheme ?? throw new ArgumentNullException(nameof(scheme)); } diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/Request/Policy.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/Request/Policy.cs index 3f88bd1fc85..dffc8884a6b 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/Request/Policy.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/Request/Policy.cs @@ -1,14 +1,17 @@ namespace HotChocolate.AspNetCore.Authorization; -public sealed class Policy +/// +/// The structure representing information about OPA policy to be evaluated by OPA server. +/// +public sealed class Policy(string path, IReadOnlyList roles) { - public Policy(string path, IReadOnlyList roles) - { - Path = path ?? throw new ArgumentNullException(nameof(path)); - Roles = roles ?? throw new ArgumentNullException(nameof(roles)); - } + /// + /// Path of the policy. Contains the path string appended to the OPA base address. + /// + public string Path { get; } = path ?? throw new ArgumentNullException(nameof(path)); - public string Path { get; } - - public IReadOnlyList Roles { get; } + /// + /// Roles associated with the user to evaluate by policy rule. + /// + public IReadOnlyList Roles { get; } = roles ?? throw new ArgumentNullException(nameof(roles)); } diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/AuthorizationAttributeTestData.cs b/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/AuthorizationAttributeTestData.cs index 9f9317f971e..d621181de44 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/AuthorizationAttributeTestData.cs +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/AuthorizationAttributeTestData.cs @@ -13,7 +13,7 @@ public class Query public string GetDefault() => "foo"; [Authorize(Policy = Policies.HasDefinedAge)] - public string GetAge() => "foo"; + public string? GetAge() => "foo"; [Authorize(Roles = ["a",])] public string GetRoles() => "foo"; @@ -40,11 +40,13 @@ private Action CreateSchema() => }) .AddOpaResultHandler( Policies.HasDefinedAge, - response => response.GetResult() switch - { - { Allow: true, } => AuthorizeResult.Allowed, - _ => AuthorizeResult.NotAllowed, - }); + response => response.DecisionId is null + ? AuthorizeResult.NotAllowed + : response.GetResult() switch + { + { Allow: true, } => AuthorizeResult.Allowed, + _ => AuthorizeResult.NotAllowed, + }); public IEnumerator GetEnumerator() { diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/AuthorizationTestData.cs b/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/AuthorizationTestData.cs index d762e9ada9c..04e37b255ce 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/AuthorizationTestData.cs +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/AuthorizationTestData.cs @@ -37,11 +37,13 @@ private Action CreateSchema() => }) .AddOpaResultHandler( Policies.HasDefinedAge, - response => response.GetResult() switch - { - { Allow: true, } => AuthorizeResult.Allowed, - _ => AuthorizeResult.NotAllowed, - }) + response => response.DecisionId is null + ? AuthorizeResult.NotAllowed + : response.GetResult() switch + { + { Allow: true, } => AuthorizeResult.Allowed, + _ => AuthorizeResult.NotAllowed, + }) .UseField(_schemaMiddleware); private Action CreateSchemaWithBuilder() => @@ -54,11 +56,13 @@ private Action CreateSchemaWithBuilder() => }) .AddOpaResultHandler( Policies.HasDefinedAge, - response => response.GetResult() switch - { - { Allow: true, } => AuthorizeResult.Allowed, - _ => AuthorizeResult.NotAllowed, - }) + response => response.DecisionId is null + ? AuthorizeResult.NotAllowed + : response.GetResult() switch + { + { Allow: true, } => AuthorizeResult.Allowed, + _ => AuthorizeResult.NotAllowed, + }) .UseField(_schemaMiddleware); public IEnumerator GetEnumerator() diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/AuthorizationTests.cs b/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/AuthorizationTests.cs index 86a58554872..029b4e945aa 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/AuthorizationTests.cs +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/AuthorizationTests.cs @@ -1,17 +1,18 @@ using System.Net; using HotChocolate.AspNetCore.Tests.Utilities; +using HotChocolate.Authorization; using HotChocolate.Execution.Configuration; +using HotChocolate.Resolvers; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.DependencyInjection; -using Opa.Native; namespace HotChocolate.AspNetCore.Authorization; public class AuthorizationTests : ServerTestBase, IAsyncLifetime { - private OpaHandle? _opaHandle; + private OpaProcess? _opaHandle; public AuthorizationTests(TestServerFactory serverFactory) : base(serverFactory) @@ -29,7 +30,7 @@ private static void SetUpHttpContext(HttpContext context) public async Task InitializeAsync() => _opaHandle = await OpaProcess.StartServerAsync(); - [Theory(Skip = "The local server needs to be packaged with squadron")] + [Theory] [ClassData(typeof(AuthorizationTestData))] [ClassData(typeof(AuthorizationAttributeTestData))] public async Task Policy_NotFound(Action configure) @@ -51,7 +52,7 @@ public async Task Policy_NotFound(Action configure) result.MatchSnapshot(); } - [Theory(Skip = "The local server needs to be packaged with squadron")] + [Theory] [ClassData(typeof(AuthorizationTestData))] [ClassData(typeof(AuthorizationAttributeTestData))] public async Task Policy_NotAuthorized(Action configure) @@ -86,7 +87,7 @@ public async Task Policy_NotAuthorized(Action configure result.MatchSnapshot(); } - [Theory(Skip = "The local server needs to be packaged with squadron")] + [Theory] [ClassData(typeof(AuthorizationTestData))] [ClassData(typeof(AuthorizationAttributeTestData))] public async Task Policy_Authorized(Action configure) @@ -122,6 +123,49 @@ public async Task Policy_Authorized(Action configure) result.MatchSnapshot(); } + [Theory] + [ClassData(typeof(AuthorizationTestData))] + [ClassData(typeof(AuthorizationAttributeTestData))] + public async Task Policy_Authorized_WithExtensions(Action configure) + { + // arrange + var server = CreateTestServer( + builder => + { + configure(builder); + builder.Services.AddAuthorization(); + builder.AddOpaQueryRequestExtensionsHandler(Policies.HasDefinedAge, + context => context.Resource is IMiddlewareContext or AuthorizationContext + ? new Dictionary { { "secret", "secret" } } + : null); + }, + SetUpHttpContext + (Action)(c => + { + // The token is the same but swapped alg and typ, + // as a result Base64 representation is not the one as expected by Rego rule + // See policies/has_age_defined.rego file for details + c.Request.Headers["Authorization"] = + "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9." + + "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJiaXJ0aGRhdGUiOiIxNy0x" + + "MS0yMDAwIn0.01Hb6X-HXl9ASf3X82Mt63RMpZ4SVJZT9hTI2dYet-k"; + })); + + var hasAgeDefinedPolicy = await File.ReadAllTextAsync("policies/has_age_defined.rego"); + using var client = new HttpClient { BaseAddress = new Uri("http://127.0.0.1:8181"), }; + + var putPolicyResponse = await client.PutAsync( + "/v1/policies/has_age_defined", + new StringContent(hasAgeDefinedPolicy)); + putPolicyResponse.EnsureSuccessStatusCode(); + + // act + var result = await server.PostAsync(new ClientQueryRequest { Query = "{ age }", }); + + // assert + Assert.Equal(HttpStatusCode.OK, result.StatusCode); + result.MatchSnapshot(); + } + private TestServer CreateTestServer( Action build, Action configureUser) diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/Claims.cs b/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/Claims.cs index 585c4d11d33..d667b63e708 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/Claims.cs +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/Claims.cs @@ -1,12 +1,18 @@ +using System.Text.Json.Serialization; + namespace HotChocolate.AspNetCore.Authorization; public class Claims { + [JsonPropertyName("birthdate")] public string Birthdate { get; set; } = default!; + [JsonPropertyName("iat")] public long Iat { get; set; } + [JsonPropertyName("name")] public string Name { get; set; } = default!; + [JsonPropertyName("sub")] public string Sub { get; set; } = default!; } diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/HasAgeDefinedResponse.cs b/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/HasAgeDefinedResponse.cs index 1f26c7b92a0..45bf4ee62b8 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/HasAgeDefinedResponse.cs +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/HasAgeDefinedResponse.cs @@ -1,8 +1,12 @@ +using System.Text.Json.Serialization; + namespace HotChocolate.AspNetCore.Authorization; public class HasAgeDefinedResponse { + [JsonPropertyName("allow")] public bool Allow { get; set; } = default!; + [JsonPropertyName("claims")] public Claims Claims { get; set; } = default!; } diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/HotChocolate.AspNetCore.Authorization.Opa.Tests.csproj b/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/HotChocolate.AspNetCore.Authorization.Opa.Tests.csproj index 6d2e1e41a02..02a418b5774 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/HotChocolate.AspNetCore.Authorization.Opa.Tests.csproj +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/HotChocolate.AspNetCore.Authorization.Opa.Tests.csproj @@ -13,11 +13,7 @@ - - - - - + Always @@ -26,4 +22,8 @@ + + + + diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/OpaProcess/OpaProcess.cs b/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/OpaProcess/OpaProcess.cs new file mode 100644 index 00000000000..ab301dcba34 --- /dev/null +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/OpaProcess/OpaProcess.cs @@ -0,0 +1,36 @@ +using DotNet.Testcontainers.Builders; +using DotNet.Testcontainers.Containers; + +namespace HotChocolate.AspNetCore.Authorization; + +public class OpaProcess +{ + private readonly IContainer _container; + + public OpaProcess(IContainer container) + { + _container = container; + } + public static async Task StartServerAsync() + { + var opaProcess = new OpaProcess(new ContainerBuilder() + .WithImage("openpolicyagent/opa") + .WithPortBinding(8181, 8181) + .WithCommand( + "run", "--server", + "--addr", ":8181", + "--log-level", "debug", + "--set", "decision_logs.console=true") + // Wait until the HTTP endpoint of the container is available. + .WithWaitStrategy(Wait.ForUnixContainer().UntilHttpRequestIsSucceeded(r => r.ForPort(8181))) + // Build the container configuration. + .Build()); + await opaProcess._container.StartAsync(); + return opaProcess; + } + + public async Task DisposeAsync() + { + await _container.DisposeAsync(); + } +} diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/Policies/has_age_defined.rego b/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/Policies/has_age_defined.rego index 70e0f0e5319..82119d20d7c 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/Policies/has_age_defined.rego +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/Policies/has_age_defined.rego @@ -7,14 +7,24 @@ import input.request default allow = { "allow" : false } -valid_jwt = [is_valid, claims] { - token := replace(request.headers["Authorization"], "Bearer ", "") +valid_jwt := [is_valid, claims] if { + token := replace(request.headers["Authorization"][0], "Bearer ", "") claims := io.jwt.decode(token)[1] - is_valid := startswith(token, "eyJhbG") # a toy validation + + exts := object.get(input, "extensions", {}) + secret := object.get(exts, "secret", "") + + is_valid := is_valid_token_or_secret(token, secret) is_valid } -allow = {"allow": is_valid, "claims": claims } { +is_valid_token_or_secret(token, secret) if { + # a toy validation + checks := { startswith(token, "eyJhbG"), secret == "secret" } # imitate OR + checks[true] +} + +allow := {"allow": is_valid, "claims": claims } if { [is_valid, claims] := valid_jwt claims.birthdate } diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/__snapshots__/AuthorizationTests.Policy_Authorized.snap b/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/__snapshots__/AuthorizationTests.Policy_Authorized.snap index 3d4b82771e9..f1bcfb78bf9 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/__snapshots__/AuthorizationTests.Policy_Authorized.snap +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/__snapshots__/AuthorizationTests.Policy_Authorized.snap @@ -1,5 +1,5 @@ { - "ContentType": "application/json; charset=utf-8", + "ContentType": "application/graphql-response+json; charset=utf-8", "StatusCode": "OK", "Data": { "age": "foo" diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/__snapshots__/AuthorizationTests.Policy_Authorized_WithExtensions.snap b/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/__snapshots__/AuthorizationTests.Policy_Authorized_WithExtensions.snap new file mode 100644 index 00000000000..f1bcfb78bf9 --- /dev/null +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/__snapshots__/AuthorizationTests.Policy_Authorized_WithExtensions.snap @@ -0,0 +1,9 @@ +{ + "ContentType": "application/graphql-response+json; charset=utf-8", + "StatusCode": "OK", + "Data": { + "age": "foo" + }, + "Errors": null, + "Extensions": null +} diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/__snapshots__/AuthorizationTests.Policy_NotAuthorized.snap b/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/__snapshots__/AuthorizationTests.Policy_NotAuthorized.snap index 39a2261460f..7290ee0650c 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/__snapshots__/AuthorizationTests.Policy_NotAuthorized.snap +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/__snapshots__/AuthorizationTests.Policy_NotAuthorized.snap @@ -1,5 +1,5 @@ { - "ContentType": "application/json; charset=utf-8", + "ContentType": "application/graphql-response+json; charset=utf-8", "StatusCode": "OK", "Data": { "age": null diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/__snapshots__/AuthorizationTests.Policy_NotFound.snap b/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/__snapshots__/AuthorizationTests.Policy_NotFound.snap index 7fb288f4848..7290ee0650c 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/__snapshots__/AuthorizationTests.Policy_NotFound.snap +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/__snapshots__/AuthorizationTests.Policy_NotFound.snap @@ -1,12 +1,12 @@ { - "ContentType": "application/json; charset=utf-8", + "ContentType": "application/graphql-response+json; charset=utf-8", "StatusCode": "OK", "Data": { "age": null }, "Errors": [ { - "message": "The `graphql/authz/has_age_defined/allow` authorization policy does not exist.", + "message": "The current user is not authorized to access this resource.", "locations": [ { "line": 1, @@ -17,7 +17,7 @@ "age" ], "extensions": { - "code": "AUTH_POLICY_NOT_FOUND" + "code": "AUTH_NOT_AUTHORIZED" } } ], From 1b6a1aba7a150f27abd0851c54281762091f977e Mon Sep 17 00:00:00 2001 From: Dmitry Stoyanovsky Date: Sun, 2 Mar 2025 16:28:54 +0100 Subject: [PATCH 2/6] rename variable --- .../AspNetCore.Authorization.Opa/OpaAuthorizationHandler.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/OpaAuthorizationHandler.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/OpaAuthorizationHandler.cs index 53fd6226256..6ca37a71ed4 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/OpaAuthorizationHandler.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/OpaAuthorizationHandler.cs @@ -47,18 +47,18 @@ public async ValueTask AuthorizeAsync( } private async ValueTask AuthorizeAsync( - OpaAuthorizationHandlerContext authContext, + OpaAuthorizationHandlerContext context, IReadOnlyList directives, CancellationToken ct) { if (directives.Count == 1) { - return await AuthorizeAsync(authContext, directives[0], ct).ConfigureAwait(false); + return await AuthorizeAsync(context, directives[0], ct).ConfigureAwait(false); } var tasks = Partitioner.Create(directives) .GetPartitions(2) - .Select(partition => ExecuteAsync(authContext, partition, AuthorizeAsync, ct)) + .Select(partition => ExecuteAsync(context, partition, AuthorizeAsync, ct)) .ToArray(); var first = await Task.WhenAny(tasks).ConfigureAwait(false); From bf399cd5ef759de525c34166eef932ab404f8fc7 Mon Sep 17 00:00:00 2001 From: Dmitry Stoyanovsky Date: Sun, 2 Mar 2025 16:38:49 +0100 Subject: [PATCH 3/6] self-review --- .../src/AspNetCore.Authorization.Opa/OpaOptions.cs | 2 -- .../Request/IPAndPort.cs | 2 +- .../Request/OpaQueryRequest.cs | 14 +++++++------- .../AspNetCore.Authorization.Opa/Request/Policy.cs | 4 ++-- 4 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/OpaOptions.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/OpaOptions.cs index 67cfd8f4c73..a0b8af88034 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/OpaOptions.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/OpaOptions.cs @@ -12,10 +12,8 @@ public sealed class OpaOptions { private readonly ConcurrentDictionary _handlerKeysRegexes = new(); - // ReSharper disable once AutoPropertyCanBeMadeGetOnly.Global public Uri BaseAddress { get; set; } = new("http://127.0.0.1:8181/v1/data/"); - // ReSharper disable once AutoPropertyCanBeMadeGetOnly.Global public TimeSpan Timeout { get; set; } = TimeSpan.FromMilliseconds(250); public JsonSerializerOptions JsonSerializerOptions { get; set; } = new(); diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/Request/IPAndPort.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/Request/IPAndPort.cs index c8c49529ec3..d19ea2c1f04 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/Request/IPAndPort.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/Request/IPAndPort.cs @@ -11,7 +11,7 @@ public sealed class IPAndPort /// /// IP address. /// Port number. - /// Thrown if port values is out of range: [0:65535]. + /// Thrown if port values is out of range: [0:65535]. public IPAndPort(string ipAddress, int port = 0) { ArgumentOutOfRangeException.ThrowIfNegativeOrZero(port); diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/Request/OpaQueryRequest.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/Request/OpaQueryRequest.cs index 84adb8af140..51f3f2432be 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/Request/OpaQueryRequest.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/Request/OpaQueryRequest.cs @@ -12,11 +12,11 @@ public sealed class OpaQueryRequest /// The instance of . /// Stores information about the original GraphQl request. /// The instance . - /// Stores information about source address of the original request + /// Stores information about the source address of the original request /// The instance . - /// Stores information about destination address of the original request. + /// Stores information about the destination address of the original request. /// The instance of object that provides extended information for the OPA query request. - /// Usually represented as a dictionary. + /// Usually is represented as a dictionary. public OpaQueryRequest( Policy policy, OriginalRequest request, @@ -36,7 +36,7 @@ public OpaQueryRequest( } /// - /// The property to get instance of . + /// The property to get the instance of . /// public OpaQueryRequestInput Input { get; } @@ -47,11 +47,11 @@ public OpaQueryRequest( /// The instance of . /// Stores information about the original GraphQl request. /// The instance . - /// Stores information about source address of the original request + /// Stores information about the source address of the original request /// The instance . - /// Stores information about destination address of the original request. + /// Stores information about the destination address of the original request. /// The instance of object the provides extended information for the OPA query request. - /// Usually represented as a dictionary. + /// Usually is represented as a dictionary. public sealed class OpaQueryRequestInput( Policy policy, OriginalRequest request, diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/Request/Policy.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/Request/Policy.cs index dffc8884a6b..0c7e689252d 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/Request/Policy.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/Request/Policy.cs @@ -1,7 +1,7 @@ namespace HotChocolate.AspNetCore.Authorization; /// -/// The structure representing information about OPA policy to be evaluated by OPA server. +/// The structure representing information about an OPA policy to be evaluated by the OPA server. /// public sealed class Policy(string path, IReadOnlyList roles) { @@ -11,7 +11,7 @@ public sealed class Policy(string path, IReadOnlyList roles) public string Path { get; } = path ?? throw new ArgumentNullException(nameof(path)); /// - /// Roles associated with the user to evaluate by policy rule. + /// Roles associated with the user to evaluate by the policy rule. /// public IReadOnlyList Roles { get; } = roles ?? throw new ArgumentNullException(nameof(roles)); } From ffaf76c04947b8bfeb0ffed126e30c1c6d282d34 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Fri, 7 Mar 2025 09:21:58 +0100 Subject: [PATCH 4/6] Update src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/Request/DefaultQueryRequestFactory.cs --- .../Request/DefaultQueryRequestFactory.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/Request/DefaultQueryRequestFactory.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/Request/DefaultQueryRequestFactory.cs index 4a781f80bdc..1decfec9c1f 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/Request/DefaultQueryRequestFactory.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/Request/DefaultQueryRequestFactory.cs @@ -23,7 +23,6 @@ public DefaultQueryRequestFactory(IOptions options) public OpaQueryRequest CreateRequest(OpaAuthorizationHandlerContext context, AuthorizeDirective directive) { ArgumentNullException.ThrowIfNull(context); - ArgumentNullException.ThrowIfNull(directive); var httpContext = From 9899e85c9246baafdb1cef05fdd0a7a58c632d46 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Fri, 7 Mar 2025 09:23:06 +0100 Subject: [PATCH 5/6] Update src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/Request/OpaQueryRequest.cs --- .../AspNetCore.Authorization.Opa/Request/OpaQueryRequest.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/Request/OpaQueryRequest.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/Request/OpaQueryRequest.cs index 51f3f2432be..2108e2920d9 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/Request/OpaQueryRequest.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/Request/OpaQueryRequest.cs @@ -25,11 +25,8 @@ public OpaQueryRequest( object? extensions = null) { ArgumentNullException.ThrowIfNull(policy); - ArgumentNullException.ThrowIfNull(request); - ArgumentNullException.ThrowIfNull(source); - ArgumentNullException.ThrowIfNull(destination); Input = new OpaQueryRequestInput(policy, request, source, destination, extensions); From 33d2bbb86b6fb1b1add90373bfa911ef5a3c3438 Mon Sep 17 00:00:00 2001 From: Glen Date: Thu, 13 Mar 2025 17:14:37 +0200 Subject: [PATCH 6/6] Corrected the "Policies" directory name --- .../AuthorizationTests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/AuthorizationTests.cs b/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/AuthorizationTests.cs index 029b4e945aa..d9a3568ba20 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/AuthorizationTests.cs +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Authorization.Opa.Tests/AuthorizationTests.cs @@ -71,7 +71,7 @@ public async Task Policy_NotAuthorized(Action configure "iwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"; })); - var hasAgeDefinedPolicy = await File.ReadAllTextAsync("policies/has_age_defined.rego"); + var hasAgeDefinedPolicy = await File.ReadAllTextAsync("Policies/has_age_defined.rego"); using var client = new HttpClient { BaseAddress = new Uri("http://127.0.0.1:8181"), }; var putPolicyResponse = await client.PutAsync( @@ -107,7 +107,7 @@ public async Task Policy_Authorized(Action configure) "jXBglnxac"; })); - var hasAgeDefinedPolicy = await File.ReadAllTextAsync("policies/has_age_defined.rego"); + var hasAgeDefinedPolicy = await File.ReadAllTextAsync("Policies/has_age_defined.rego"); using var client = new HttpClient { BaseAddress = new Uri("http://127.0.0.1:8181"), }; var putPolicyResponse = await client.PutAsync( @@ -150,7 +150,7 @@ public async Task Policy_Authorized_WithExtensions(Action