Skip to content

Commit

Permalink
fix: OPA result field serialization
Browse files Browse the repository at this point in the history
fix: mapping no default policy and policy not found responses
  • Loading branch information
queil committed Feb 2, 2022
1 parent 8117bdb commit 279e2ee
Show file tree
Hide file tree
Showing 11 changed files with 113 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ public class DefaultOpaDecision : IOpaDecision
public AuthorizeResult Map(ResponseBase? response) => response switch
{
QueryResponse { Result: true } => AuthorizeResult.Allowed,
NoDefaultPolicy => AuthorizeResult.NoDefaultPolicy,
PolicyNotFound => AuthorizeResult.PolicyNotFound,
NoDefaultPolicy => AuthorizeResult.NoDefaultPolicy,
_ => AuthorizeResult.NotAllowed
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,4 @@
<ProjectReference Include="..\..\..\Core\src\Types\HotChocolate.Types.csproj" />
<ProjectReference Include="..\AspNetCore.Authorization\HotChocolate.AspNetCore.Authorization.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ namespace HotChocolate.AspNetCore.Authorization;

public sealed class OpaOptions
{
public Uri BaseAddress { get; set; } = new("http://127.0.0.1:8181");
public Uri BaseAddress { get; set; } = new("http://127.0.0.1:8181/v1/data/");
public TimeSpan ConnectionTimeout { get; set; } = TimeSpan.FromMilliseconds(250);
public JsonSerializerOptions JsonSerializerOptions { get; set; } = new();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using System.Text.Json;
using System.Text.Json.Serialization;

namespace HotChocolate.AspNetCore.Authorization.Opa;

/// <summary>
/// Opa Result Converter
/// </summary>
/// <remarks>
/// As described in https://www.openpolicyagent.org/docs/latest/rest-api/#get-a-document
/// The server returns 200 if the path refers to an undefined document.
/// In this case, the response will not contain a result property.
/// The property is actually returned as an empty object '{ }'.
/// Therefore, it can't be deserialized as nullable boolean by default, hence this converter.
/// </remarks>
public class OpaResultFieldConverter : JsonConverter<bool?>
{
public override bool? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartObject) return reader.GetBoolean();
reader.Skip();
return null;
}

public override void Write(Utf8JsonWriter writer, bool? value, JsonSerializerOptions options)
{
if (value is { } v) writer.WriteBooleanValue(v);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,13 @@ public OpaService(HttpClient httpClient, OpaOptions options)
if (request is null) throw new ArgumentNullException(nameof(request));

HttpResponseMessage response = await _httpClient.PostAsync(policyPath, request.ToJsonContent(_options.JsonSerializerOptions), token);
if (policyPath.Equals(string.Empty) && response.StatusCode == HttpStatusCode.NotFound) return NoDefaultPolicy.Response;
return await response.Content.QueryResponseFromJsonAsync(_options.JsonSerializerOptions, token);
response.EnsureSuccessStatusCode();
QueryResponse? result = await response.Content.QueryResponseFromJsonAsync(_options.JsonSerializerOptions, token);
return result switch
{
{ Result: null } when policyPath.Equals(string.Empty) => NoDefaultPolicy.Response,
{ Result: null } => PolicyNotFound.Response,
var r => r
};
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
using System.Text.Json.Serialization;
using HotChocolate.AspNetCore.Authorization.Opa;

namespace HotChocolate.AspNetCore.Authorization;

public abstract class ResponseBase { }

public sealed class QueryResponse : ResponseBase
{
public Guid? DecisionId { get; set; }
public bool Result { get; set; }
[JsonConverter(typeof(OpaResultFieldConverter))]
public bool? Result { get; set; }
}

public sealed class PolicyNotFound : ResponseBase
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Text.Json;
using HotChocolate.Execution.Configuration;
using Microsoft.Extensions.DependencyInjection;

Expand All @@ -14,7 +13,7 @@ public class Query
[Authorize]
public string GetDefault() => "foo";

[Authorize(Policy = "HasAgeDefined")]
[Authorize(Policy = "graphql/authz/has_age_defined/allow")]
public string GetAge() => "foo";

[Authorize(Roles = new[] { "a" })]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public class AuthorizationTestData : IEnumerable<object[]>
private readonly string SchemaCode = @"
type Query {
default: String @authorize
age: String @authorize(policy: ""HasAgeDefined"")
age: String @authorize(policy: ""graphql/authz/has_age_defined/allow"")
roles: String @authorize(roles: [""a""])
roles_ab: String @authorize(roles: [""a"" ""b""])
piped: String
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Threading;
using System.Net.Http;
using System.Threading.Tasks;
using HotChocolate.AspNetCore.Utilities;
using HotChocolate.Execution.Configuration;
using HotChocolate.Resolvers;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.TestHost;
Expand All @@ -19,12 +15,23 @@

namespace HotChocolate.AspNetCore.Authorization;

public class AuthorizationTests : ServerTestBase
public class AuthorizationTests : ServerTestBase, IAsyncLifetime
{
private OpaHandle _opaHandle;
public AuthorizationTests(TestServerFactory serverFactory)
: base(serverFactory)
{
}
private static void SetUpHttpContext(HttpContext context)
{
ConnectionInfo connection = context.Connection;
connection.LocalIpAddress = IPAddress.Loopback;
connection.LocalPort = 5555;
connection.RemoteIpAddress = IPAddress.Loopback;
connection.RemotePort = 7777;
}

public async Task InitializeAsync() => _opaHandle = await OpaProcess.StartServerAsync();

[Theory]
[ClassData(typeof(AuthorizationTestData))]
Expand All @@ -38,20 +45,45 @@ public async Task DefaultPolicy_NotFound(Action<IRequestExecutorBuilder> configu
configure(builder);
builder.Services.AddAuthorization();
},
context =>
SetUpHttpContext);

// act
ClientQueryResult result =
await server.PostAsync(new ClientQueryRequest { Query = "{ default }" });

// assert
Assert.Equal(HttpStatusCode.OK, result.StatusCode);
result.MatchSnapshot();
}

[Theory]
[ClassData(typeof(AuthorizationTestData))]
[ClassData(typeof(AuthorizationAttributeTestData))]
public async Task Policy_NotAuthorized(Action<IRequestExecutorBuilder> configure)
{
// arrange
TestServer server = CreateTestServer(
builder =>
{
ConnectionInfo connection = context.Request.HttpContext.Connection;
connection.LocalIpAddress = IPAddress.Loopback;
connection.LocalPort = 5555;
connection.RemoteIpAddress = IPAddress.Loopback;
connection.RemotePort = 7777;
});
configure(builder);
builder.Services.AddAuthorization();

},
SetUpHttpContext + (Action<HttpContext>)(c =>
{
c.Request.Headers["Authorization"] =
"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";
}));

await using OpaHandle _ = await OpaProcess.StartServerAsync();
var hasAgeDefinedPolicy = await File.ReadAllTextAsync("policies/has_age_defined.rego");
using var client = new HttpClient { BaseAddress = new Uri("http://127.0.0.1:8181") };

HttpResponseMessage putPolicyResponse = await client.PutAsync("/v1/policies/has_age_defined", new StringContent(hasAgeDefinedPolicy));
putPolicyResponse.EnsureSuccessStatusCode();

// act
ClientQueryResult result =
await server.PostAsync(new ClientQueryRequest { Query = "{ default }" });
await server.PostAsync(new ClientQueryRequest { Query = "{ age }" });

// assert
Assert.Equal(HttpStatusCode.OK, result.StatusCode);
Expand Down Expand Up @@ -81,4 +113,6 @@ private TestServer CreateTestServer(
app.UseEndpoints(b => b.MapGraphQL());
});
}

public async Task DisposeAsync() => await _opaHandle.DisposeAsync();
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,16 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Opa.Native" Version="0.36.6" PrivateAssets="Compile" IncludeAssets="Compile;Runtime;Native" />
<PackageReference Include="Opa.Native" Version="0.37.1" PrivateAssets="Compile" IncludeAssets="Compile;Runtime;Native" />
</ItemGroup>

<!--For Visual Studio for Mac Test Explorer we need this reference here-->
<ItemGroup>
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
</ItemGroup>

<ItemGroup>
<None Include="Policies/*.rego" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package graphql.authz.has_age_defined

import input.request

default allow = false

input["token"] = replace(request.headers["Authorization"], "Bearer ", "")

claims := io.jwt.decode(input.token)[1]

allow {
claims.birthdate
}

0 comments on commit 279e2ee

Please sign in to comment.