diff --git a/Package.Build.props b/Package.Build.props index f1be50f..5801518 100644 --- a/Package.Build.props +++ b/Package.Build.props @@ -1,6 +1,6 @@ - 0.4.0-alpha + 0.5.0-alpha Hawxy true Apache-2.0 diff --git a/build.cmd b/build.cmd new file mode 100755 index 0000000..b08cc59 --- /dev/null +++ b/build.cmd @@ -0,0 +1,7 @@ +:; set -eo pipefail +:; SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd) +:; ${SCRIPT_DIR}/build.sh "$@" +:; exit $? + +@ECHO OFF +powershell -ExecutionPolicy ByPass -NoProfile -File "%~dp0build.ps1" %* diff --git a/build.ps1 b/build.ps1 index 1c774e5..8c52d63 100644 --- a/build.ps1 +++ b/build.ps1 @@ -63,7 +63,7 @@ else { $env:DOTNET_EXE = "$DotNetDirectory\dotnet.exe" } -Write-Output "Microsoft (R) .NET Core SDK version $(& $env:DOTNET_EXE --version)" +Write-Output "Microsoft (R) .NET SDK version $(& $env:DOTNET_EXE --version)" ExecSafe { & $env:DOTNET_EXE build $BuildProjectFile /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary --verbosity quiet } ExecSafe { & $env:DOTNET_EXE run --project $BuildProjectFile --no-build -- $BuildArguments } diff --git a/build.sh b/build.sh index e8961f9..1f3ba09 100755 --- a/build.sh +++ b/build.sh @@ -56,7 +56,7 @@ else export DOTNET_EXE="$DOTNET_DIRECTORY/dotnet" fi -echo "Microsoft (R) .NET Core SDK version $("$DOTNET_EXE" --version)" +echo "Microsoft (R) .NET SDK version $("$DOTNET_EXE" --version)" "$DOTNET_EXE" build "$BUILD_PROJECT_FILE" /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary --verbosity quiet "$DOTNET_EXE" run --project "$BUILD_PROJECT_FILE" --no-build -- "$@" diff --git a/build/Build.cs b/build/Build.cs index 2c0800a..d9df69e 100644 --- a/build/Build.cs +++ b/build/Build.cs @@ -14,7 +14,6 @@ using static Nuke.Common.IO.PathConstruction; using static Nuke.Common.Tools.DotNet.DotNetTasks; -[CheckBuildProjectConfigurations] [ShutdownDotNetAfterServerBuild] class Build : NukeBuild { diff --git a/build/_build.csproj b/build/_build.csproj index 32702d7..be8500f 100644 --- a/build/_build.csproj +++ b/build/_build.csproj @@ -11,7 +11,7 @@ - + diff --git a/samples/Fga.Example.AspNetCore/Fga.Example.AspNetCore.csproj b/samples/Fga.Example.AspNetCore/Fga.Example.AspNetCore.csproj index 9923c00..fa4d9e1 100644 --- a/samples/Fga.Example.AspNetCore/Fga.Example.AspNetCore.csproj +++ b/samples/Fga.Example.AspNetCore/Fga.Example.AspNetCore.csproj @@ -8,7 +8,7 @@ - + diff --git a/src/Fga.Net.AspNetCore/Authorization/FgaCheckDecorator.cs b/src/Fga.Net.AspNetCore/Authorization/FgaCheckDecorator.cs new file mode 100644 index 0000000..a506864 --- /dev/null +++ b/src/Fga.Net.AspNetCore/Authorization/FgaCheckDecorator.cs @@ -0,0 +1,43 @@ +using Auth0.Fga.Api; +using Auth0.Fga.Model; + +namespace Fga.Net.AspNetCore.Authorization; + +/// +/// Temporary wrapper to allow for easier testing of middleware. Don't take a dependency on this. +/// +public class FgaCheckDecorator : IFgaCheckDecorator +{ + private readonly Auth0FgaApi _auth0FgaApi; + + /// + /// + /// + /// + public FgaCheckDecorator(Auth0FgaApi auth0FgaApi) + { + _auth0FgaApi = auth0FgaApi; + } + + /// + /// + /// + /// + /// + /// + public virtual Task Check(CheckRequest request, CancellationToken ct) => _auth0FgaApi.Check(request, ct); +} + +/// +/// Temporary wrapper to allow for easier testing of middleware. Don't take a dependency on this. +/// +public interface IFgaCheckDecorator +{ + /// + /// + /// + /// + /// + /// + Task Check(CheckRequest request, CancellationToken ct); +} \ No newline at end of file diff --git a/src/Fga.Net.AspNetCore/Authorization/FineGrainedAuthorizationHandler.cs b/src/Fga.Net.AspNetCore/Authorization/FineGrainedAuthorizationHandler.cs index 45febff..71493a0 100644 --- a/src/Fga.Net.AspNetCore/Authorization/FineGrainedAuthorizationHandler.cs +++ b/src/Fga.Net.AspNetCore/Authorization/FineGrainedAuthorizationHandler.cs @@ -16,24 +16,24 @@ limitations under the License. */ #endregion -using Auth0.Fga.Api; using Auth0.Fga.Model; using Fga.Net.AspNetCore.Authorization.Attributes; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; namespace Fga.Net.AspNetCore.Authorization; internal class FineGrainedAuthorizationHandler : AuthorizationHandler { - private readonly Auth0FgaApi _client; + private readonly IFgaCheckDecorator _client; + private readonly ILogger _logger; - public FineGrainedAuthorizationHandler(Auth0FgaApi client) + public FineGrainedAuthorizationHandler(IFgaCheckDecorator client, ILogger logger) { _client = client; + _logger = logger; } - - protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, FineGrainedAuthorizationRequirement requirement) { if (context.Resource is HttpContext httpContext) @@ -45,7 +45,6 @@ protected override async Task HandleRequirementAsync(AuthorizationHandlerContext // The user is enforcing the fga policy but there's no attributes here. if (attributes.Count == 0) return; - var results = new List(); foreach (var attribute in attributes) { var user = await attribute.GetUser(httpContext); @@ -55,7 +54,7 @@ protected override async Task HandleRequirementAsync(AuthorizationHandlerContext if (string.IsNullOrEmpty(user) || string.IsNullOrEmpty(relation) || string.IsNullOrEmpty(@object)) return; - var result = await _client.Check(new CheckRequestParams() + var result = await _client.Check(new CheckRequest() { TupleKey = new TupleKey { @@ -65,13 +64,13 @@ protected override async Task HandleRequirementAsync(AuthorizationHandlerContext } }, httpContext.RequestAborted); - results.Add(result.Allowed); + if (!result.Allowed) + { + _logger.CheckFailureDebug(user, relation, @object); + return; + } } - - if(results.All(x => x)) - context.Succeed(requirement); + context.Succeed(requirement); } } } - - diff --git a/src/Fga.Net.AspNetCore/Authorization/FineGrainedAuthorizationRequirement.cs b/src/Fga.Net.AspNetCore/Authorization/FineGrainedAuthorizationRequirement.cs index 81c8b81..fa679f5 100644 --- a/src/Fga.Net.AspNetCore/Authorization/FineGrainedAuthorizationRequirement.cs +++ b/src/Fga.Net.AspNetCore/Authorization/FineGrainedAuthorizationRequirement.cs @@ -22,4 +22,7 @@ namespace Fga.Net.AspNetCore.Authorization; internal class FineGrainedAuthorizationRequirement : IAuthorizationRequirement { + public override string ToString() => + $"{nameof(FineGrainedAuthorizationRequirement)}: Requires FGA Authorization checks to pass."; + } \ No newline at end of file diff --git a/src/Fga.Net.AspNetCore/Authorization/Log.cs b/src/Fga.Net.AspNetCore/Authorization/Log.cs new file mode 100644 index 0000000..03aa4dc --- /dev/null +++ b/src/Fga.Net.AspNetCore/Authorization/Log.cs @@ -0,0 +1,27 @@ +#region License +/* + Copyright 2021-2022 Hawxy + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ +#endregion + +using Microsoft.Extensions.Logging; + +namespace Fga.Net.AspNetCore.Authorization; + +internal static partial class Log +{ + [LoggerMessage(0, LogLevel.Debug, "FGA Check failed for User: {user}, Relation: {relation}, Object: {object}")] + public static partial void CheckFailureDebug(this ILogger logger, string user, string relation, string @object); +} \ No newline at end of file diff --git a/src/Fga.Net.AspNetCore/Controllers/FgaControllerBase.cs b/src/Fga.Net.AspNetCore/Controllers/FgaControllerBase.cs index 4048152..dd43c12 100644 --- a/src/Fga.Net.AspNetCore/Controllers/FgaControllerBase.cs +++ b/src/Fga.Net.AspNetCore/Controllers/FgaControllerBase.cs @@ -47,7 +47,7 @@ public FgaControllerBase(Auth0FgaApi client) /// public async Task Check(string user, string relation, string @object, CancellationToken ct) { - var checkRes = await _client.Check(new CheckRequestParams() + var checkRes = await _client.Check(new CheckRequest() { TupleKey = new TupleKey { diff --git a/src/Fga.Net.AspNetCore/ServiceCollectionExtensions.cs b/src/Fga.Net.AspNetCore/ServiceCollectionExtensions.cs index 14ac98c..2e9d6a9 100644 --- a/src/Fga.Net.AspNetCore/ServiceCollectionExtensions.cs +++ b/src/Fga.Net.AspNetCore/ServiceCollectionExtensions.cs @@ -40,6 +40,7 @@ public static IServiceCollection AddAuth0Fga(this IServiceCollection collection, ArgumentNullException.ThrowIfNull(config); collection.AddAuth0FgaClient(config); + collection.AddScoped(); collection.AddScoped(); return collection; } diff --git a/src/Fga.Net/Fga.Net.DependencyInjection.csproj b/src/Fga.Net/Fga.Net.DependencyInjection.csproj index 6e5bcd3..1a922af 100644 --- a/src/Fga.Net/Fga.Net.DependencyInjection.csproj +++ b/src/Fga.Net/Fga.Net.DependencyInjection.csproj @@ -12,7 +12,7 @@ - + diff --git a/tests/Fga.Net.Tests/Client/EndpointTests.cs b/tests/Fga.Net.Tests/Client/EndpointTests.cs index 1bff1f2..4f29381 100644 --- a/tests/Fga.Net.Tests/Client/EndpointTests.cs +++ b/tests/Fga.Net.Tests/Client/EndpointTests.cs @@ -23,13 +23,19 @@ private async Task GetEndpoints_Return_200() { using var scope = _host.Services.CreateScope(); var client = scope.ServiceProvider.GetRequiredService(); - var modelIds = await client.ReadAuthorizationModels(); + var modelsResponse = await client.ReadAuthorizationModels(); - Assert.NotNull(modelIds); - Assert.NotNull(modelIds.AuthorizationModelIds); - Assert.True(modelIds.AuthorizationModelIds?.Count > 0); + Assert.NotNull(modelsResponse); + Assert.NotNull(modelsResponse.AuthorizationModels); + Assert.True(modelsResponse.AuthorizationModels?.Count > 0); + + var modelId = modelsResponse.AuthorizationModels?.First().Id!; + + var modelResponse = await client.ReadAuthorizationModel(modelId); + + Assert.NotNull(modelResponse); + Assert.NotNull(modelResponse.AuthorizationModel.Id); - var modelId = modelIds.AuthorizationModelIds?.First()!; var assertions = await client.ReadAssertions(modelId); Assert.NotNull(assertions); @@ -40,7 +46,7 @@ private async Task GetEndpoints_Return_200() Assert.NotEmpty(assertion.Relation!); Assert.NotEmpty(assertion.User!); - var graph = await client.Expand(new ExpandRequestParams() + var graph = await client.Expand(new ExpandRequest() { AuthorizationModelId = modelId, TupleKey = assertion @@ -51,7 +57,6 @@ private async Task GetEndpoints_Return_200() var watch = await client.ReadChanges(); Assert.NotNull(watch); - } diff --git a/tests/Fga.Net.Tests/Fga.Net.Tests.csproj b/tests/Fga.Net.Tests/Fga.Net.Tests.csproj index 99c61f2..cecc254 100644 --- a/tests/Fga.Net.Tests/Fga.Net.Tests.csproj +++ b/tests/Fga.Net.Tests/Fga.Net.Tests.csproj @@ -9,10 +9,10 @@ - - + + - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/tests/Fga.Net.Tests/Middleware/MiddlewareTests.cs b/tests/Fga.Net.Tests/Middleware/MiddlewareTests.cs index 54ad1cd..73c1d0e 100644 --- a/tests/Fga.Net.Tests/Middleware/MiddlewareTests.cs +++ b/tests/Fga.Net.Tests/Middleware/MiddlewareTests.cs @@ -19,7 +19,7 @@ public MiddlewareTests(WebAppFixture fixture) { _alba = fixture.AlbaHost; } - [Fact(Skip="Moq is broke")] + [Fact] public async Task Authorization_HappyPath_Succeeds() { await _alba.Scenario(_ => @@ -28,7 +28,7 @@ await _alba.Scenario(_ => _.StatusCodeShouldBeOk(); }); } - [Fact(Skip = "Moq is broke")] + [Fact] public async Task Authorization_UnhappyPath_Forbidden() { await _alba.Scenario(_ => diff --git a/tests/Fga.Net.Tests/Middleware/WebAppFixture.cs b/tests/Fga.Net.Tests/Middleware/WebAppFixture.cs index bd9530a..4f0f480 100644 --- a/tests/Fga.Net.Tests/Middleware/WebAppFixture.cs +++ b/tests/Fga.Net.Tests/Middleware/WebAppFixture.cs @@ -1,8 +1,8 @@ using System.Threading; using System.Threading.Tasks; using Alba; -using Auth0.Fga.Api; using Auth0.Fga.Model; +using Fga.Net.AspNetCore.Authorization; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Moq; @@ -16,12 +16,12 @@ public class WebAppFixture : IAsyncLifetime public async Task InitializeAsync() { - var authorizationClientMock = new Mock(); + var authorizationClientMock = new Mock(); authorizationClientMock.Setup(c => - c.Check(It.IsAny(), + c.Check(It.IsAny(), It.IsAny())) - .ReturnsAsync((string _, CheckRequestParams res, CancellationToken _) => + .ReturnsAsync((CheckRequest res, CancellationToken _) => res.TupleKey!.User == MockJwtConfiguration.DefaultUser ? new CheckResponse() { Allowed = true } : new CheckResponse() { Allowed = false }); @@ -31,7 +31,7 @@ public async Task InitializeAsync() { builder.ConfigureServices(s => { - s.Replace(ServiceDescriptor.Scoped(_ => authorizationClientMock.Object)); + s.Replace(ServiceDescriptor.Scoped(_ => authorizationClientMock.Object)); }); }, MockJwtConfiguration.GetDefaultStubConfiguration());