Skip to content

Commit 2b53375

Browse files
stoyanovskydmitryglen-84
authored andcommitted
[OPA] Fix OPA middleware to comply with OPA V1 (#8084)
Co-authored-by: Glen <glen.84@gmail.com>
1 parent da68fb3 commit 2b53375

26 files changed

+465
-193
lines changed

src/Directory.Packages.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@
3939
<PackageVersion Include="Newtonsoft.Json" Version="13.0.2" />
4040
<PackageVersion Include="NodaTime" Version="3.0.0" />
4141
<PackageVersion Include="Npgsql" Version="8.0.4" />
42-
<PackageVersion Include="Opa.Native" Version="0.41.0" />
4342
<PackageVersion Include="OpenTelemetry.Api" Version="1.1.0" />
4443
<PackageVersion Include="ProjNET" Version="2.0.0" />
4544
<PackageVersion Include="RabbitMQ.Client" Version="6.4.0" />
@@ -59,6 +58,7 @@
5958
<PackageVersion Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
6059
<PackageVersion Include="System.ComponentModel.Annotations" Version="5.0.0" />
6160
<PackageVersion Include="System.Reactive" Version="6.0.0" />
61+
<PackageVersion Include="Testcontainers" Version="4.3.0" />
6262
<PackageVersion Include="xunit" Version="2.9.3" />
6363
<PackageVersion Include="xunit.assert" Version="2.9.3" />
6464
<PackageVersion Include="xunit.extensibility.core" Version="2.9.3" />

src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/Extensions/HotChocolateAuthorizeRequestExecutorBuilder.cs

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
using HotChocolate.AspNetCore.Authorization;
22
using HotChocolate.Execution.Configuration;
3-
using Microsoft.Extensions.Configuration;
43
using System.Text.Json;
54
using System.Text.Json.Serialization;
5+
using Microsoft.Extensions.Configuration;
66
using Microsoft.Extensions.Options;
77

88
namespace Microsoft.Extensions.DependencyInjection;
@@ -42,7 +42,7 @@ public static IRequestExecutorBuilder AddOpaAuthorization(
4242
var jsonOptions = new JsonSerializerOptions
4343
{
4444
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
45-
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
45+
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
4646
};
4747
jsonOptions.Converters.Add(
4848
new JsonStringEnumConverter(
@@ -55,6 +55,15 @@ public static IRequestExecutorBuilder AddOpaAuthorization(
5555
return builder;
5656
}
5757

58+
/// <summary>
59+
/// Adds result handler to the OPA options.
60+
/// </summary>
61+
/// <param name="builder">Instance of <see cref="IRequestExecutorBuilder"/>.</param>
62+
/// <param name="policyPath">The path to the policy.</param>
63+
/// <param name="parseResult">The PDP decision result.</param>
64+
/// <returns>
65+
/// Returns the <see cref="IRequestExecutorBuilder"/> for chaining in more configurations.
66+
/// </returns>
5867
public static IRequestExecutorBuilder AddOpaResultHandler(
5968
this IRequestExecutorBuilder builder,
6069
string policyPath,
@@ -66,4 +75,27 @@ public static IRequestExecutorBuilder AddOpaResultHandler(
6675
(o, _) => o.PolicyResultHandlers.Add(policyPath, parseResult));
6776
return builder;
6877
}
78+
79+
/// <summary>
80+
/// Adds OPA query request extensions handler to the OPA options.
81+
/// </summary>
82+
/// <param name="builder">Instance of <see cref="IRequestExecutorBuilder"/>.</param>
83+
/// <param name="policyPath">The path to the policy.</param>
84+
/// <param name="opaQueryRequestExtensionsHandler">The handler for the extensions associated with the Policy.
85+
/// </param>
86+
/// <returns>
87+
/// Returns the <see cref="IRequestExecutorBuilder"/> for chaining in more configurations.
88+
/// </returns>
89+
public static IRequestExecutorBuilder AddOpaQueryRequestExtensionsHandler(
90+
this IRequestExecutorBuilder builder,
91+
string policyPath,
92+
OpaQueryRequestExtensionsHandler opaQueryRequestExtensionsHandler)
93+
{
94+
builder.Services
95+
.AddOptions<OpaOptions>()
96+
.Configure<IServiceProvider>(
97+
(o, _) => o.OpaQueryRequestExtensionsHandlers.Add(policyPath, opaQueryRequestExtensionsHandler));
98+
return builder;
99+
}
100+
69101
}

src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/IOpaService.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,17 @@
11
namespace HotChocolate.AspNetCore.Authorization;
22

3+
/// <summary>
4+
/// The OPA service interface communicating with OPA server.
5+
/// </summary>
36
public interface IOpaService
47
{
8+
/// <summary>
9+
/// The method used to query OPA PDP decision based on the request input.
10+
/// </summary>
11+
/// <param name="policyPath">The string parameter representing path of the evaluating policy.</param>
12+
/// <param name="request">The instance <see cref="OpaQueryRequest"/>.</param>
13+
/// <param name="ct">Cancellation token.</param>
14+
/// <returns></returns>
515
Task<OpaQueryResponse> QueryAsync(
616
string policyPath,
717
OpaQueryRequest request,

src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/OpaAuthorizationHandler.cs

Lines changed: 19 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -19,43 +19,36 @@ public OpaAuthorizationHandler(
1919
IOpaQueryRequestFactory requestFactory,
2020
IOptions<OpaOptions> options)
2121
{
22-
if (options is null)
23-
{
24-
throw new ArgumentNullException(nameof(options));
25-
}
22+
ArgumentNullException.ThrowIfNull(options);
2623

2724
_opaService = opaService ?? throw new ArgumentNullException(nameof(opaService));
2825
_requestFactory = requestFactory ?? throw new ArgumentNullException(nameof(requestFactory));
2926
_options = options.Value;
3027
}
3128

32-
/// <summary>
33-
/// Authorize current directive using OPA (Open Policy Agent).
34-
/// </summary>
35-
/// <param name="context">The current middleware context.</param>
36-
/// <param name="directive">The authorization directive.</param>
37-
/// <param name="ct">The cancellation token.</param>
38-
/// <returns>
39-
/// Returns a value indicating if the current session is authorized to
40-
/// access the resolver data.
41-
/// </returns>
29+
/// <inheritdoc/>
4230
public async ValueTask<AuthorizeResult> AuthorizeAsync(
4331
IMiddlewareContext context,
4432
AuthorizeDirective directive,
45-
CancellationToken ct)
33+
CancellationToken cancellationToken = default)
4634
{
47-
var authorizationContext = new AuthorizationContext(
48-
context.Schema,
49-
context.Services,
50-
context.ContextData,
51-
context.Operation.Document,
52-
context.Operation.Id);
53-
return await AuthorizeAsync(authorizationContext, directive, ct).ConfigureAwait(false);
35+
return await AuthorizeAsync(
36+
new OpaAuthorizationHandlerContext(context), [directive], cancellationToken).ConfigureAwait(false);
5437
}
5538

39+
/// <inheritdoc/>
5640
public async ValueTask<AuthorizeResult> AuthorizeAsync(
5741
AuthorizationContext context,
5842
IReadOnlyList<AuthorizeDirective> directives,
43+
CancellationToken cancellationToken = default)
44+
{
45+
return await AuthorizeAsync(
46+
new OpaAuthorizationHandlerContext(context), directives, cancellationToken).ConfigureAwait(false);
47+
}
48+
49+
private async ValueTask<AuthorizeResult> AuthorizeAsync(
50+
OpaAuthorizationHandlerContext context,
51+
IReadOnlyList<AuthorizeDirective> directives,
5952
CancellationToken ct)
6053
{
6154
if (directives.Count == 1)
@@ -89,12 +82,12 @@ public async ValueTask<AuthorizeResult> AuthorizeAsync(
8982
return AuthorizeResult.Allowed;
9083

9184
static async Task<AuthorizeResult> ExecuteAsync(
92-
AuthorizationContext context,
85+
OpaAuthorizationHandlerContext context,
9386
IEnumerator<AuthorizeDirective> partition,
9487
Authorize authorize,
9588
CancellationToken ct)
9689
{
97-
while (partition.MoveNext() && partition.Current is not null)
90+
while (partition.MoveNext())
9891
{
9992
var directive = partition.Current;
10093
var result = await authorize(context, directive, ct).ConfigureAwait(false);
@@ -110,7 +103,7 @@ static async Task<AuthorizeResult> ExecuteAsync(
110103
}
111104

112105
private async ValueTask<AuthorizeResult> AuthorizeAsync(
113-
AuthorizationContext context,
106+
OpaAuthorizationHandlerContext context,
114107
AuthorizeDirective directive,
115108
CancellationToken ct)
116109
{
@@ -122,7 +115,7 @@ private async ValueTask<AuthorizeResult> AuthorizeAsync(
122115
}
123116

124117
private delegate ValueTask<AuthorizeResult> Authorize(
125-
AuthorizationContext context,
118+
OpaAuthorizationHandlerContext context,
126119
AuthorizeDirective directive,
127120
CancellationToken ct);
128121
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
namespace HotChocolate.AspNetCore.Authorization;
2+
3+
/// <summary>
4+
/// The OPA authorization handler context.
5+
/// </summary>
6+
public class OpaAuthorizationHandlerContext
7+
{
8+
/// <summary>
9+
/// The constructor.
10+
/// </summary>
11+
/// <param name="resource">Either IMiddlewareContext or AuthorizationContext depending on the phase of
12+
/// a rule execution.
13+
/// </param>
14+
public OpaAuthorizationHandlerContext(object resource)
15+
{
16+
ArgumentNullException.ThrowIfNull(resource);
17+
18+
Resource = resource;
19+
}
20+
21+
/// <summary>
22+
/// The object representing instance of either IMiddlewareContext or AuthorizationContext.
23+
/// </summary>
24+
public object Resource { get; }
25+
}

src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/OpaOptions.cs

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55

66
namespace HotChocolate.AspNetCore.Authorization;
77

8+
/// <summary>
9+
/// The class representing OPA configuration options.
10+
/// </summary>
811
public sealed class OpaOptions
912
{
1013
private readonly ConcurrentDictionary<string, Regex> _handlerKeysRegexes = new();
@@ -17,14 +20,34 @@ public sealed class OpaOptions
1720

1821
public Dictionary<string, ParseResult> PolicyResultHandlers { get; } = new();
1922

23+
public Dictionary<string, OpaQueryRequestExtensionsHandler> OpaQueryRequestExtensionsHandlers { get; } = new();
24+
25+
public OpaQueryRequestExtensionsHandler? GetOpaQueryRequestExtensionsHandler(string policyPath)
26+
{
27+
if (OpaQueryRequestExtensionsHandlers.Count == 0)
28+
{
29+
return null;
30+
}
31+
return OpaQueryRequestExtensionsHandlers.TryGetValue(policyPath, out var handler)
32+
? handler :
33+
FindHandler(policyPath, OpaQueryRequestExtensionsHandlers);
34+
}
35+
2036
public ParseResult GetPolicyResultParser(string policyPath)
2137
{
2238
if (PolicyResultHandlers.TryGetValue(policyPath, out var handler))
2339
{
2440
return handler;
2541
}
42+
handler = FindHandler(policyPath, PolicyResultHandlers);
43+
return handler ??
44+
throw new InvalidOperationException(
45+
$"No result handler found for policy: {policyPath}");
46+
}
2647

27-
var maybeHandler = PolicyResultHandlers.SingleOrDefault(
48+
private THandler? FindHandler<THandler>(string policyPath, Dictionary<string, THandler> handlers)
49+
{
50+
var maybeHandler = handlers.SingleOrDefault(
2851
k =>
2952
{
3053
var regex = _handlerKeysRegexes.GetOrAdd(
@@ -33,14 +56,15 @@ public ParseResult GetPolicyResultParser(string policyPath)
3356
k.Key,
3457
RegexOptions.Compiled |
3558
RegexOptions.Singleline |
36-
RegexOptions.CultureInvariant));
59+
RegexOptions.CultureInvariant,
60+
TimeSpan.FromMilliseconds(500)));
3761
return regex.IsMatch(policyPath);
3862
});
3963

40-
return maybeHandler.Value ??
41-
throw new InvalidOperationException(
42-
$"No result handler found for policy: {policyPath}");
64+
return maybeHandler.Value;
4365
}
4466
}
4567

4668
public delegate AuthorizeResult ParseResult(OpaQueryResponse response);
69+
70+
public delegate object? OpaQueryRequestExtensionsHandler(OpaAuthorizationHandlerContext context);

src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/OpaService.cs

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,36 +11,29 @@ internal sealed class OpaService : IOpaService
1111

1212
public OpaService(HttpClient httpClient, IOptions<OpaOptions> options)
1313
{
14-
if (options is null)
15-
{
16-
throw new ArgumentNullException(nameof(options));
17-
}
14+
ArgumentNullException.ThrowIfNull(options);
1815

19-
_client = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
16+
ArgumentNullException.ThrowIfNull(httpClient);
17+
18+
_client = httpClient;
2019
_options = options.Value;
2120
}
2221

2322
public async Task<OpaQueryResponse> QueryAsync(
2423
string policyPath,
2524
OpaQueryRequest request,
26-
CancellationToken ct)
25+
CancellationToken cancellationToken = default)
2726
{
28-
if (policyPath is null)
29-
{
30-
throw new ArgumentNullException(nameof(policyPath));
31-
}
27+
ArgumentNullException.ThrowIfNull(policyPath);
3228

33-
if (request is null)
34-
{
35-
throw new ArgumentNullException(nameof(request));
36-
}
29+
ArgumentNullException.ThrowIfNull(request);
3730

3831
using var body = JsonContent.Create(request, options: _options.JsonSerializerOptions);
3932

40-
using var response = await _client.PostAsync(policyPath, body, ct).ConfigureAwait(false);
33+
using var response = await _client.PostAsync(policyPath, body, cancellationToken).ConfigureAwait(false);
4134
response.EnsureSuccessStatusCode();
42-
await using var stream = await response.Content.ReadAsStreamAsync(ct).ConfigureAwait(false);
43-
var document = await JsonDocument.ParseAsync(stream, default, ct);
35+
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
36+
var document = await JsonDocument.ParseAsync(stream, default, cancellationToken);
4437
return new OpaQueryResponse(document);
4538
}
4639
}

src/HotChocolate/AspNetCore/src/AspNetCore.Authorization.Opa/QueryResponse.cs

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,20 @@
22

33
namespace HotChocolate.AspNetCore.Authorization;
44

5-
public sealed class OpaQueryResponse : IDisposable
5+
/// <summary>
6+
/// The class representing OPA query response.
7+
/// </summary>
8+
public sealed class OpaQueryResponse(JsonDocument document) : IDisposable
69
{
7-
private readonly JsonDocument _document;
8-
private readonly JsonElement _root;
9-
10-
public OpaQueryResponse(JsonDocument document)
11-
{
12-
_document = document;
13-
_root = document.RootElement;
14-
}
10+
private readonly JsonElement _root = document.RootElement;
1511

1612
public Guid? DecisionId
17-
=> _root.TryGetProperty("decisionId", out var value)
13+
=> _root.TryGetProperty("decision_id", out var value)
1814
? value.GetGuid()
1915
: null;
2016

2117
public T? GetResult<T>()
22-
=> _root.TryGetProperty("decisionId", out var value)
18+
=> _root.TryGetProperty("result", out var value)
2319
? value.Deserialize<T>()
2420
: default;
2521

@@ -28,5 +24,5 @@ public bool IsEmpty
2824
_root.EnumerateObject().Any();
2925

3026
public void Dispose()
31-
=> _document.Dispose();
27+
=> document.Dispose();
3228
}

0 commit comments

Comments
 (0)