diff --git a/.github/workflows/Build_&_Test.yml b/.github/workflows/Build_&_Test.yml index f60b2db..256c10b 100644 --- a/.github/workflows/Build_&_Test.yml +++ b/.github/workflows/Build_&_Test.yml @@ -29,9 +29,12 @@ jobs: name: ubuntu-latest runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: '9' - name: 'Cache: .nuke/temp, ~/.nuget/packages' - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | .nuke/temp diff --git a/.github/workflows/Manual_Nuget_Push.yml b/.github/workflows/Manual_Nuget_Push.yml index 799fe86..4299d2a 100644 --- a/.github/workflows/Manual_Nuget_Push.yml +++ b/.github/workflows/Manual_Nuget_Push.yml @@ -23,9 +23,12 @@ jobs: name: ubuntu-latest runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: '9' - name: 'Cache: .nuke/temp, ~/.nuget/packages' - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | .nuke/temp diff --git a/.nuke/build.schema.json b/.nuke/build.schema.json index 68c5085..412ee68 100644 --- a/.nuke/build.schema.json +++ b/.nuke/build.schema.json @@ -1,64 +1,65 @@ { "$schema": "http://json-schema.org/draft-04/schema#", - "$ref": "#/definitions/build", - "title": "Build Schema", "definitions": { - "build": { - "type": "object", + "Host": { + "type": "string", + "enum": [ + "AppVeyor", + "AzurePipelines", + "Bamboo", + "Bitbucket", + "Bitrise", + "GitHubActions", + "GitLab", + "Jenkins", + "Rider", + "SpaceAutomation", + "TeamCity", + "Terminal", + "TravisCI", + "VisualStudio", + "VSCode" + ] + }, + "ExecutableTarget": { + "type": "string", + "enum": [ + "Clean", + "Compile", + "NugetPack", + "NugetPush", + "Restore", + "Test" + ] + }, + "Verbosity": { + "type": "string", + "description": "", + "enum": [ + "Verbose", + "Normal", + "Minimal", + "Quiet" + ] + }, + "NukeBuild": { "properties": { "Continue": { "type": "boolean", "description": "Indicates to continue a previously failed build attempt" }, - "FgaClientId": { - "type": "string", - "description": "FGA Client ID", - "default": "Secrets must be entered via 'nuke :secrets [profile]'" - }, - "FgaClientSecret": { - "type": "string", - "description": "FGA Client Secret", - "default": "Secrets must be entered via 'nuke :secrets [profile]'" - }, - "FgaStoreId": { - "type": "string", - "description": "FGA Store ID", - "default": "Secrets must be entered via 'nuke :secrets [profile]'" - }, "Help": { "type": "boolean", "description": "Shows the help text for this build assembly" }, "Host": { - "type": "string", "description": "Host for execution. Default is 'automatic'", - "enum": [ - "AppVeyor", - "AzurePipelines", - "Bamboo", - "Bitbucket", - "Bitrise", - "GitHubActions", - "GitLab", - "Jenkins", - "Rider", - "SpaceAutomation", - "TeamCity", - "Terminal", - "TravisCI", - "VisualStudio", - "VSCode" - ] + "$ref": "#/definitions/Host" }, "NoLogo": { "type": "boolean", "description": "Disables displaying the NUKE logo" }, - "NugetApiKey": { - "type": "string", - "description": "Nuget Api Key", - "default": "Secrets must be entered via 'nuke :secrets [profile]'" - }, "Partition": { "type": "string", "description": "Partition to use on CI" @@ -82,47 +83,54 @@ "type": "array", "description": "List of targets to be skipped. Empty list skips all dependencies", "items": { - "type": "string", - "enum": [ - "Clean", - "Compile", - "NugetPack", - "NugetPush", - "Restore", - "Test" - ] + "$ref": "#/definitions/ExecutableTarget" } }, - "Solution": { - "type": "string", - "description": "Path to a solution file that is automatically loaded" - }, "Target": { "type": "array", "description": "List of targets to be invoked. Default is '{default_target}'", "items": { - "type": "string", - "enum": [ - "Clean", - "Compile", - "NugetPack", - "NugetPush", - "Restore", - "Test" - ] + "$ref": "#/definitions/ExecutableTarget" } }, "Verbosity": { - "type": "string", "description": "Logging verbosity during build execution. Default is 'Normal'", - "enum": [ - "Minimal", - "Normal", - "Quiet", - "Verbose" - ] + "$ref": "#/definitions/Verbosity" + } + } + } + }, + "allOf": [ + { + "properties": { + "FgaClientId": { + "type": "string", + "description": "FGA Client ID", + "default": "Secrets must be entered via 'nuke :secrets [profile]'" + }, + "FgaClientSecret": { + "type": "string", + "description": "FGA Client Secret", + "default": "Secrets must be entered via 'nuke :secrets [profile]'" + }, + "FgaStoreId": { + "type": "string", + "description": "FGA Store ID", + "default": "Secrets must be entered via 'nuke :secrets [profile]'" + }, + "NugetApiKey": { + "type": "string", + "description": "Nuget Api Key", + "default": "Secrets must be entered via 'nuke :secrets [profile]'" + }, + "Solution": { + "type": "string", + "description": "Path to a solution file that is automatically loaded" } } + }, + { + "$ref": "#/definitions/NukeBuild" } - } + ] } diff --git a/Package.Build.props b/Package.Build.props index d6bf39f..765873a 100644 --- a/Package.Build.props +++ b/Package.Build.props @@ -1,6 +1,6 @@ - 1.2.0 + 2.0.0-RC.1 Hawxy true Apache-2.0 diff --git a/README.md b/README.md index 066d1e1..a1b62c9 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ ## Getting Started -This package is compatible with the OSS OpenFGA as well as the managed Auth0 FGA service. Usage of DSL v1.1 is required. +This package is compatible with the OSS OpenFGA as well as the managed Auth0 FGA service. Please ensure you have a basic understanding of how FGA works before continuing: [OpenFGA Docs](https://openfga.dev/) or [Auth0 FGA Docs](https://docs.fga.dev/) @@ -174,6 +174,10 @@ public class ComputedRelationshipAttribute : FgaBaseObjectAttribute An additional pre-made attribute that allows all tuple values to be hardcoded strings ships with the package (`FgaStringAttribute`). This attribute is useful for testing and debug purposes, but should not be used in a real application. +### Contextual Tuples + +All attributes supports specifying contextual tuples as part of a check. Inherit & override `GetContextualTuple` to provide the relevant logic in your own attribute. + ## Client Injection This package registers both the `OpenFgaApi` and `OpenFgaClient` types in the DI container. `OpenFgaClient` is a higher level abstraction and preferred over `OpenFgaApi` for general use. diff --git a/build/Build.cs b/build/Build.cs index 4b04d6c..977000b 100644 --- a/build/Build.cs +++ b/build/Build.cs @@ -13,6 +13,7 @@ [GitHubActions( "Build & Test", GitHubActionsImage.UbuntuLatest, + AutoGenerate = false, OnPushBranches = new []{ "main" }, OnPullRequestBranches = new []{ "main" }, InvokedTargets = new[] { nameof(Test) }, @@ -20,6 +21,7 @@ [GitHubActions( "Manual Nuget Push", GitHubActionsImage.UbuntuLatest, + AutoGenerate = false, On = new[] { GitHubActionsTrigger.WorkflowDispatch }, InvokedTargets = new[] { nameof(NugetPush) }, ImportSecrets = new[] { nameof(NugetApiKey) })] diff --git a/build/_build.csproj b/build/_build.csproj index c72030f..446c9fd 100644 --- a/build/_build.csproj +++ b/build/_build.csproj @@ -11,7 +11,7 @@ - + diff --git a/samples/Fga.Example.AspNetCore/ComputedRelationshipAttribute.cs b/samples/Fga.Example.AspNetCore/ComputedRelationshipAttribute.cs index 5349d06..3a1fca2 100644 --- a/samples/Fga.Example.AspNetCore/ComputedRelationshipAttribute.cs +++ b/samples/Fga.Example.AspNetCore/ComputedRelationshipAttribute.cs @@ -1,4 +1,5 @@ using Fga.Net.AspNetCore.Authorization.Attributes; +using OpenFga.Sdk.Client.Model; namespace Fga.Example.AspNetCore; diff --git a/src/Fga.Net.AspNetCore/Authorization/Attributes/FgaAttribute.cs b/src/Fga.Net.AspNetCore/Authorization/Attributes/FgaAttribute.cs index 054b3a7..a43075d 100644 --- a/src/Fga.Net.AspNetCore/Authorization/Attributes/FgaAttribute.cs +++ b/src/Fga.Net.AspNetCore/Authorization/Attributes/FgaAttribute.cs @@ -17,6 +17,8 @@ limitations under the License. #endregion using Microsoft.AspNetCore.Http; +using OpenFga.Sdk.Client.Model; +using Tuple = OpenFga.Sdk.Model.Tuple; namespace Fga.Net.AspNetCore.Authorization.Attributes; @@ -42,8 +44,15 @@ public abstract class FgaAttribute : Attribute /// An entity in the system. /// /// The context of the current request - /// Usually a string in an entity-identifier format: document:id + /// Usually a string in an entity-identifier format: document:id public abstract ValueTask GetObject(HttpContext context); + + /// + /// Contextual tuple(s) to apply the check generated from this attribute. + /// + /// The context of the current request + /// The list of contextual tuples, or null if none were provided + public virtual ValueTask?> GetContextualTuple(HttpContext context) => new((List?)null); /// /// Concats the type and identifier into the object format @@ -51,6 +60,6 @@ public abstract class FgaAttribute : Attribute /// The objects type, such as workspace, repository, organization or document /// The objects identifier /// The object in the entity:identifier format - public static string FormatObject(string type, string identifier) => $"{type}:{identifier}"; + protected static string FormatObject(string type, string identifier) => $"{type}:{identifier}"; } diff --git a/src/Fga.Net.AspNetCore/Authorization/FineGrainedAuthorizationHandler.cs b/src/Fga.Net.AspNetCore/Authorization/FineGrainedAuthorizationHandler.cs index e7b228a..2141d88 100644 --- a/src/Fga.Net.AspNetCore/Authorization/FineGrainedAuthorizationHandler.cs +++ b/src/Fga.Net.AspNetCore/Authorization/FineGrainedAuthorizationHandler.cs @@ -48,7 +48,7 @@ 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 checks = new List(); foreach (var attribute in attributes) @@ -56,11 +56,13 @@ protected override async Task HandleRequirementAsync(AuthorizationHandlerContext string? user; string? relation; string? @object; + List? contextualTuples; try { user = await attribute.GetUser(httpContext); relation = await attribute.GetRelation(httpContext); @object = await attribute.GetObject(httpContext); + contextualTuples = await attribute.GetContextualTuple(httpContext); } catch (FgaMiddlewareException ex) { @@ -85,7 +87,8 @@ protected override async Task HandleRequirementAsync(AuthorizationHandlerContext { User = user, Relation = relation, - Object = @object + Object = @object, + ContextualTuples = contextualTuples }); } diff --git a/src/Fga.Net.AspNetCore/Authorization/Log.cs b/src/Fga.Net.AspNetCore/Authorization/Log.cs index 9a7f24a..2ff58b4 100644 --- a/src/Fga.Net.AspNetCore/Authorization/Log.cs +++ b/src/Fga.Net.AspNetCore/Authorization/Log.cs @@ -37,7 +37,5 @@ internal static partial class Log [LoggerMessage(3005, LogLevel.Debug, "User was not in a valid format of 'type:id' or '*'. Computed user as '{user}'")] public static partial void InvalidUser(this ILogger logger, string user); - - } \ No newline at end of file diff --git a/src/Fga.Net.AspNetCore/Fga.Net.AspNetCore.csproj b/src/Fga.Net.AspNetCore/Fga.Net.AspNetCore.csproj index 0e35314..798b330 100644 --- a/src/Fga.Net.AspNetCore/Fga.Net.AspNetCore.csproj +++ b/src/Fga.Net.AspNetCore/Fga.Net.AspNetCore.csproj @@ -1,7 +1,7 @@  - net6.0;net7.0;net8.0 + net8.0;net9.0 enable enable @@ -17,7 +17,7 @@ - + diff --git a/src/Fga.Net/Configuration/Auth0FgaConnectionBuilder.cs b/src/Fga.Net/Configuration/Auth0FgaConnectionBuilder.cs index 6f81c66..7f4a728 100644 --- a/src/Fga.Net/Configuration/Auth0FgaConnectionBuilder.cs +++ b/src/Fga.Net/Configuration/Auth0FgaConnectionBuilder.cs @@ -48,10 +48,10 @@ internal sealed record Auth0FgaEnvironment(string ApiHost, string ApiTokenIssuer /// public sealed class Auth0FgaConnectionBuilder { - private const string FgaIssuer = "fga.us.auth0.com"; + private const string FgaIssuer = "auth.fga.dev"; - private readonly IReadOnlyDictionary _fgaEnvironments = - new Dictionary() + private readonly Dictionary _fgaEnvironments = + new() { { FgaEnvironment.US, diff --git a/src/Fga.Net/Configuration/OpenFgaConnectionBuilder.cs b/src/Fga.Net/Configuration/OpenFgaConnectionBuilder.cs index 58b9170..1fd0c73 100644 --- a/src/Fga.Net/Configuration/OpenFgaConnectionBuilder.cs +++ b/src/Fga.Net/Configuration/OpenFgaConnectionBuilder.cs @@ -26,20 +26,7 @@ namespace Fga.Net.DependencyInjection.Configuration; public sealed class OpenFgaConnectionBuilder { private string? _apiUrl; - - /// - /// Sets the connection configuration for the host. - /// - /// API scheme, either http or https. - /// API host, should be in be plain URI format - /// - [Obsolete("Passing in a split scheme & host is obsolete and will be removed in a future release. Use SetConnection(string apiUrl)")] - public OpenFgaConnectionBuilder SetConnection(string apiScheme, string apiHost) - { - _apiUrl = $"{apiScheme}://{apiHost}"; - return this; - } - + /// /// Sets the connection configuration for the host. /// diff --git a/src/Fga.Net/Fga.Net.DependencyInjection.csproj b/src/Fga.Net/Fga.Net.DependencyInjection.csproj index 2c35f73..b7a6ee7 100644 --- a/src/Fga.Net/Fga.Net.DependencyInjection.csproj +++ b/src/Fga.Net/Fga.Net.DependencyInjection.csproj @@ -1,6 +1,6 @@ - net6.0;net8.0 + net8.0;net9.0 enable enable @@ -12,17 +12,17 @@ - - - - - - + + + + + + \ No newline at end of file diff --git a/src/Fga.Net/FgaConfigurationBuilder.cs b/src/Fga.Net/FgaConfigurationBuilder.cs index f7f6815..50f546d 100644 --- a/src/Fga.Net/FgaConfigurationBuilder.cs +++ b/src/Fga.Net/FgaConfigurationBuilder.cs @@ -18,6 +18,7 @@ limitations under the License. using Fga.Net.DependencyInjection.Configuration; using OpenFga.Sdk.Client; +using OpenFga.Sdk.Configuration; namespace Fga.Net.DependencyInjection; @@ -62,7 +63,15 @@ public FgaConfigurationBuilder SetWaitInMs(int waitInMs) _minWaitInMs = waitInMs; return this; } + + private TelemetryConfig? _telemetryConfig; + /// + public FgaConfigurationBuilder SetTelemetry(TelemetryConfig config) + { + _telemetryConfig = config; + return this; + } /// /// Configures the client for use with OpenFga @@ -90,6 +99,7 @@ public void ConfigureAuth0Fga(Action config) config.Invoke(configuration); _fgaConfiguration = configuration.Build(); } + internal FgaBuiltConfiguration Build() { if (_fgaConfiguration is null) @@ -97,6 +107,6 @@ internal FgaBuiltConfiguration Build() if (string.IsNullOrEmpty(_storeId)) throw new InvalidOperationException("Store ID must be set"); - return new FgaBuiltConfiguration(_storeId, _authorizationModelId, _maxRetry, _minWaitInMs, _fgaConfiguration); + return new FgaBuiltConfiguration(_storeId, _authorizationModelId, _maxRetry, _minWaitInMs, _telemetryConfig, _fgaConfiguration); } } \ No newline at end of file diff --git a/src/Fga.Net/FgaConnectionConfiguration.cs b/src/Fga.Net/FgaConnectionConfiguration.cs index 72e4d6a..52759c3 100644 --- a/src/Fga.Net/FgaConnectionConfiguration.cs +++ b/src/Fga.Net/FgaConnectionConfiguration.cs @@ -26,6 +26,7 @@ internal sealed record FgaBuiltConfiguration( string? AuthorizationModelId, int? MaxRetry, int? MinWaitInMs, + TelemetryConfig? TelemetryConfig, FgaConnectionConfiguration Connection); internal sealed record FgaConnectionConfiguration(string ApiUrl, Credentials? Credentials); \ No newline at end of file diff --git a/src/Fga.Net/ServiceCollectionExtensions.cs b/src/Fga.Net/ServiceCollectionExtensions.cs index 81c9912..3126f07 100644 --- a/src/Fga.Net/ServiceCollectionExtensions.cs +++ b/src/Fga.Net/ServiceCollectionExtensions.cs @@ -89,6 +89,8 @@ private static void ConfigureFgaOptions(this FgaClientConfiguration x, FgaBuiltC if (config.MinWaitInMs.HasValue) x.MinWaitInMs = config.MinWaitInMs.Value; + x.Telemetry = config.TelemetryConfig; + x.Credentials = config.Connection.Credentials; } diff --git a/tests/Fga.Net.Tests/Fga.Net.Tests.csproj b/tests/Fga.Net.Tests/Fga.Net.Tests.csproj index 33319d8..3cc8775 100644 --- a/tests/Fga.Net.Tests/Fga.Net.Tests.csproj +++ b/tests/Fga.Net.Tests/Fga.Net.Tests.csproj @@ -8,12 +8,12 @@ - + - - - - + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all