diff --git a/.github/workflows/build-and-test.yaml b/.github/workflows/build-and-test.yaml deleted file mode 100644 index 4f2c763..0000000 --- a/.github/workflows/build-and-test.yaml +++ /dev/null @@ -1,39 +0,0 @@ -name: Build and Test - -on: - push: - pull_request: - workflow_dispatch: - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - - - name: Setup .NET - uses: actions/setup-dotnet@v1 - with: - dotnet-version: 6.0.x - - - name: Restore dependencies - run: dotnet restore - - - name: Build - run: dotnet build --no-restore --configuration Release - - - name: Test - run: dotnet test --no-build --verbosity normal --configuration Release - - - name: Run Snyk to check for vulnerabilities - uses: snyk/actions/dotnet@master - env: - SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} - with: - args: src/Com.Auth0.FGA/ - - - uses: actions/upload-artifact@v2 - with: - name: nuget-package - path: src/Com.Auth0.FGA/bin/Release/Com.Auth0.FGA.*.nupkg diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml new file mode 100644 index 0000000..c1c7932 --- /dev/null +++ b/.github/workflows/main.yaml @@ -0,0 +1,83 @@ +name: Build, Test and Release + +on: + push: + pull_request: + workflow_dispatch: + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - name: Setup .NET + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 6.0.x + + - name: Restore dependencies + run: dotnet restore + + - name: Build + run: dotnet build --no-restore + + - name: Test + run: dotnet test --no-build --verbosity normal + + - name: Run Snyk to check for vulnerabilities + uses: snyk/actions/dotnet@master + env: + SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} + with: + args: src/Com.Auth0.FGA/ + + - uses: actions/upload-artifact@v2 + with: + name: nuget-package + path: src/Com.Auth0.FGA/bin/Release/Com.Auth0.FGA.*.nupkg + + publish: + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/v') + needs: test + + steps: + - uses: actions/checkout@v2 + + - name: Setup .NET + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 6.0.x + source-url: https://nuget.pkg.github.com/auth0-lab/index.json + env: + NUGET_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}} + + - name: Restore dependencies + run: dotnet restore + + - name: Build + run: dotnet build --no-restore --configuration Release + + - uses: actions/upload-artifact@v2 + with: + name: nuget-package + path: src/Com.Auth0.FGA/bin/Release/Com.Auth0.FGA.*.nupkg + + - name: Publish + run: dotnet nuget push src/Com.Auth0.FGA/bin/Release/Com.Auth0.FGA.$($env:GITHUB_REF -replace "refs/tags/v").nupkg + + create-release: + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/v') + needs: publish + + steps: + - uses: actions/checkout@v2 + + - uses: Roang-zero1/github-create-release-action@5cf058ddffa6fa04e5cda07c98570c757dc4a0e1 + with: + version_regex: ^v[[:digit:]]+\.[[:digit:]]+\.[[:digit:]]+ + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.openapi-generator/FILES b/.openapi-generator/FILES index 2f58686..3624136 100644 --- a/.openapi-generator/FILES +++ b/.openapi-generator/FILES @@ -5,7 +5,7 @@ .github/ISSUE_TEMPLATES/feature_request.md .github/PULL_REQUEST_TEMPLATE.md .github/SECURITY.md -.github/workflows/build-and-test.yaml +.github/workflows/main.yaml .github/workflows/semgrep.yaml .gitignore CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md index ab5c00f..958c4b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog -## 0.1.0 (2022-02-09) +## v0.1.1 +### [0.1.1](https://github.com/auth0-lab/fga-dotnet-sdk/compare/v0.1.0...v0.1.1) (2022-03-09) + +#### Changes +- fix: fix for return types on 204 no content + +## v0.1.1 + +### 0.1.0 (2022-03-07) + +#### Changes Initial Release diff --git a/docs/Auth0FgaApi.md b/docs/Auth0FgaApi.md index c16500c..6b71987 100644 --- a/docs/Auth0FgaApi.md +++ b/docs/Auth0FgaApi.md @@ -97,13 +97,12 @@ Name | Type | Description | Notes | **404** | Request failed due to incorrect path. | - | | **429** | Request failed due to too many requests. | - | | **500** | Request failed due to internal server error. | - | -| **0** | An unexpected error response. | - | [[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) # **DeleteTokenIssuer** -> Object DeleteTokenIssuer (string id) +> void DeleteTokenIssuer (string id) Remove 3rd party token issuer for Auth0 FGA read and write operations @@ -138,8 +137,7 @@ namespace Example try { // Remove 3rd party token issuer for Auth0 FGA read and write operations - Object response = await auth0FgaApi.DeleteTokenIssuer(id); - Debug.WriteLine(response); + auth0FgaApi.DeleteTokenIssuer(id); } catch (ApiException e) { @@ -161,7 +159,7 @@ Name | Type | Description | Notes ### Return type -**Object** +void (empty response body) ### HTTP request headers @@ -172,7 +170,6 @@ Name | Type | Description | Notes ### HTTP response details | Status code | Description | Response headers | |-------------|-------------|------------------| -| **200** | A successful response. | - | | **204** | A successful response. | - | | **400** | Request failed due to invalid input. | - | | **401** | Request failed due to authentication errors. | - | @@ -180,7 +177,6 @@ Name | Type | Description | Notes | **404** | Request failed due to incorrect path. | - | | **429** | Request failed due to too many requests. | - | | **500** | Request failed due to internal server error. | - | -| **0** | An unexpected error response. | - | [[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) @@ -262,7 +258,6 @@ Name | Type | Description | Notes | **404** | Request failed due to incorrect path. | - | | **429** | Request failed due to too many requests. | - | | **500** | Request failed due to internal server error. | - | -| **0** | An unexpected error response. | - | [[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) @@ -344,7 +339,6 @@ Name | Type | Description | Notes | **404** | Request failed due to incorrect path. | - | | **429** | Request failed due to too many requests. | - | | **500** | Request failed due to internal server error. | - | -| **0** | An unexpected error response. | - | [[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) @@ -426,7 +420,6 @@ Name | Type | Description | Notes | **404** | Request failed due to incorrect path. | - | | **429** | Request failed due to too many requests. | - | | **500** | Request failed due to internal server error. | - | -| **0** | An unexpected error response. | - | [[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) @@ -508,7 +501,6 @@ Name | Type | Description | Notes | **404** | Request failed due to incorrect path. | - | | **429** | Request failed due to too many requests. | - | | **500** | Request failed due to internal server error. | - | -| **0** | An unexpected error response. | - | [[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) @@ -592,7 +584,6 @@ Name | Type | Description | Notes | **404** | Request failed due to incorrect path. | - | | **429** | Request failed due to too many requests. | - | | **500** | Request failed due to internal server error. | - | -| **0** | An unexpected error response. | - | [[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) @@ -672,7 +663,6 @@ Name | Type | Description | Notes | **404** | Request failed due to incorrect path. | - | | **429** | Request failed due to too many requests. | - | | **500** | Request failed due to internal server error. | - | -| **0** | An unexpected error response. | - | [[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) @@ -754,13 +744,12 @@ Name | Type | Description | Notes | **404** | Request failed due to incorrect path. | - | | **429** | Request failed due to too many requests. | - | | **500** | Request failed due to internal server error. | - | -| **0** | An unexpected error response. | - | [[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) # **WriteAssertions** -> Object WriteAssertions (string authorizationModelId, WriteAssertionsRequestParams body) +> void WriteAssertions (string authorizationModelId, WriteAssertionsRequestParams body) Upsert assertions for an authorization model ID @@ -796,8 +785,7 @@ namespace Example try { // Upsert assertions for an authorization model ID - Object response = await auth0FgaApi.WriteAssertions(authorizationModelId, body); - Debug.WriteLine(response); + auth0FgaApi.WriteAssertions(authorizationModelId, body); } catch (ApiException e) { @@ -820,7 +808,7 @@ Name | Type | Description | Notes ### Return type -**Object** +void (empty response body) ### HTTP request headers @@ -831,7 +819,6 @@ Name | Type | Description | Notes ### HTTP response details | Status code | Description | Response headers | |-------------|-------------|------------------| -| **200** | A successful response. | - | | **204** | A successful response. | - | | **400** | Request failed due to invalid input. | - | | **401** | Request failed due to authentication errors. | - | @@ -839,7 +826,6 @@ Name | Type | Description | Notes | **404** | Request failed due to incorrect path. | - | | **429** | Request failed due to too many requests. | - | | **500** | Request failed due to internal server error. | - | -| **0** | An unexpected error response. | - | [[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) @@ -914,7 +900,6 @@ Name | Type | Description | Notes ### HTTP response details | Status code | Description | Response headers | |-------------|-------------|------------------| -| **200** | A successful response. | - | | **201** | A successful response. | - | | **400** | Request failed due to invalid input. | - | | **401** | Request failed due to authentication errors. | - | @@ -922,7 +907,6 @@ Name | Type | Description | Notes | **404** | Request failed due to incorrect path. | - | | **429** | Request failed due to too many requests. | - | | **500** | Request failed due to internal server error. | - | -| **0** | An unexpected error response. | - | [[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) @@ -1004,7 +988,6 @@ Name | Type | Description | Notes | **404** | Request failed due to incorrect path. | - | | **429** | Request failed due to too many requests. | - | | **500** | Request failed due to internal server error. | - | -| **0** | An unexpected error response. | - | [[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) @@ -1079,7 +1062,6 @@ Name | Type | Description | Notes ### HTTP response details | Status code | Description | Response headers | |-------------|-------------|------------------| -| **200** | A successful response. | - | | **201** | A successful response. | - | | **400** | Request failed due to invalid input. | - | | **401** | Request failed due to authentication errors. | - | @@ -1087,7 +1069,6 @@ Name | Type | Description | Notes | **404** | Request failed due to incorrect path. | - | | **429** | Request failed due to too many requests. | - | | **500** | Request failed due to internal server error. | - | -| **0** | An unexpected error response. | - | [[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) diff --git a/src/Com.Auth0.FGA.Test/Api/Auth0FgaApiTests.cs b/src/Com.Auth0.FGA.Test/Api/Auth0FgaApiTests.cs index db9e08d..29f0242 100644 --- a/src/Com.Auth0.FGA.Test/Api/Auth0FgaApiTests.cs +++ b/src/Com.Auth0.FGA.Test/Api/Auth0FgaApiTests.cs @@ -801,8 +801,7 @@ public async Task WriteAssertionsTest() { ItExpr.IsAny() ) .ReturnsAsync(new HttpResponseMessage() { - StatusCode = HttpStatusCode.OK, - Content = Utils.CreateJsonStringContent(new Object()), + StatusCode = HttpStatusCode.NoContent, }); var httpClient = new HttpClient(mockHandler.Object); @@ -811,7 +810,7 @@ public async Task WriteAssertionsTest() { var body = new WriteAssertionsRequestParams(assertions: new List() { new(new TupleKey("repo:auth0/express-jwt", "reader", "anne"), true) }); - var response = await auth0FgaApi.WriteAssertions(authorizationModelId, body); + await auth0FgaApi.WriteAssertions(authorizationModelId, body); mockHandler.Protected().Verify( "SendAsync", @@ -1010,14 +1009,13 @@ public async Task DeleteTokenIssuerTest() { ItExpr.IsAny() ) .ReturnsAsync(new HttpResponseMessage() { - StatusCode = HttpStatusCode.OK, - Content = Utils.CreateJsonStringContent(new Object()), + StatusCode = HttpStatusCode.NoContent, }); var httpClient = new HttpClient(mockHandler.Object); var auth0FgaApi = new Auth0FgaApi(_config, httpClient); - var response = await auth0FgaApi.DeleteTokenIssuer(issuerId); + await auth0FgaApi.DeleteTokenIssuer(issuerId); mockHandler.Protected().Verify( "SendAsync", diff --git a/src/Com.Auth0.FGA/Api/Auth0FgaApi.cs b/src/Com.Auth0.FGA/Api/Auth0FgaApi.cs index 5051035..1d7cef6 100644 --- a/src/Com.Auth0.FGA/Api/Auth0FgaApi.cs +++ b/src/Com.Auth0.FGA/Api/Auth0FgaApi.cs @@ -89,8 +89,8 @@ public async Task Check(CheckRequestParams body, CancellationToke /// Thrown when fails to make API call /// Id of token issuer to be removed /// Cancellation Token to cancel the request. - /// Task of Object - public async Task DeleteTokenIssuer(string id, CancellationToken cancellationToken = default) { + /// Task of void + public async Task DeleteTokenIssuer(string id, CancellationToken cancellationToken = default) { var pathParams = new Dictionary { { "store_id", _configuration.StoreId } }; if (id != null) { @@ -106,7 +106,7 @@ public async Task DeleteTokenIssuer(string id, CancellationToken cancell QueryParameters = queryParams }; - return await this._apiClient.SendRequestAsync(requestBuilder, + await this._apiClient.SendRequestAsync(requestBuilder, "DeleteTokenIssuer", cancellationToken); } @@ -300,8 +300,8 @@ public async Task Write(WriteRequestParams body, CancellationToken cance /// /// /// Cancellation Token to cancel the request. - /// Task of Object - public async Task WriteAssertions(string authorizationModelId, WriteAssertionsRequestParams body, CancellationToken cancellationToken = default) { + /// Task of void + public async Task WriteAssertions(string authorizationModelId, WriteAssertionsRequestParams body, CancellationToken cancellationToken = default) { var pathParams = new Dictionary { { "store_id", _configuration.StoreId } }; if (authorizationModelId != null) { @@ -318,7 +318,7 @@ public async Task WriteAssertions(string authorizationModelId, WriteAsse QueryParameters = queryParams }; - return await this._apiClient.SendRequestAsync(requestBuilder, + await this._apiClient.SendRequestAsync(requestBuilder, "WriteAssertions", cancellationToken); } diff --git a/src/Com.Auth0.FGA/Client/ApiClient.cs b/src/Com.Auth0.FGA/Client/ApiClient.cs index 4aaa7cb..3ad8e5f 100644 --- a/src/Com.Auth0.FGA/Client/ApiClient.cs +++ b/src/Com.Auth0.FGA/Client/ApiClient.cs @@ -17,11 +17,19 @@ namespace Com.Auth0.FGA.Client; +/// +/// API Client - used by all the API related methods to call the API. Handles token exchange and retries. +/// public class ApiClient : IDisposable { private readonly BaseClient _baseClient; private readonly OAuth2Client? _oauth2Client; private readonly Configuration.Configuration _configuration; + /// + /// Initializes a new instance of the class. + /// + /// Client Configuration + /// User Http Client - Allows Http Client reuse public ApiClient(Configuration.Configuration configuration, HttpClient? userHttpClient = null) { configuration.IsValid(); _configuration = configuration; @@ -32,6 +40,15 @@ public ApiClient(Configuration.Configuration configuration, HttpClient? userHttp } } + /// + /// Handles getting the access token, calling the API and potentially retrying + /// + /// + /// + /// + /// Response Type + /// + /// public async Task SendRequestAsync(RequestBuilder requestBuilder, string apiName, CancellationToken cancellationToken = default) { IDictionary additionalHeaders = new Dictionary(); @@ -51,6 +68,32 @@ public async Task SendRequestAsync(RequestBuilder requestBuilder, string a return await Retry(async () => await _baseClient.SendRequestAsync(requestBuilder, additionalHeaders, apiName, cancellationToken)); } + /// + /// Handles getting the access token, calling the API and potentially retrying (use for requests that return no content) + /// + /// + /// + /// + /// + public async Task SendRequestAsync(RequestBuilder requestBuilder, string apiName, + CancellationToken cancellationToken = default) { + IDictionary additionalHeaders = new Dictionary(); + + if (_oauth2Client != null) { + try { + var token = await _oauth2Client.GetAccessTokenAsync(); + + if (!string.IsNullOrEmpty(token)) { + additionalHeaders.Add("Authorization", $"Bearer {token}"); + } + } catch (ApiException e) { + throw new Auth0FgaAuthenticationError("Invalid Client Credentials", apiName, e); + } + } + + await Retry(async () => await _baseClient.SendRequestAsync(requestBuilder, additionalHeaders, apiName, cancellationToken)); + } + private async Task Retry(Func> retryable) { var numRetries = 0; while (true) { @@ -71,6 +114,28 @@ private async Task Retry(Func> retryable) { } } + private async Task Retry(Func retryable) { + var numRetries = 0; + while (true) { + try { + numRetries++; + + await retryable(); + + return; + } catch (Auth0FgaApiRateLimitExceededError err) { + if (numRetries > _configuration.MaxRetry) { + throw; + } + var waitInMs = (int) ((err.ResetInMs == null || err.ResetInMs < _configuration.MinWaitInMs) + ? _configuration.MinWaitInMs + : err.ResetInMs); + + await Task.Delay(waitInMs); + } + } + } + public void Dispose() { _baseClient.Dispose(); } diff --git a/src/Com.Auth0.FGA/Client/BaseClient.cs b/src/Com.Auth0.FGA/Client/BaseClient.cs index 7872c24..1b82d41 100644 --- a/src/Com.Auth0.FGA/Client/BaseClient.cs +++ b/src/Com.Auth0.FGA/Client/BaseClient.cs @@ -53,7 +53,7 @@ public BaseClient(Configuration.Configuration configuration, HttpClient? httpCli } /// - /// + /// Handles calling the API /// /// /// @@ -70,7 +70,23 @@ public async Task SendRequestAsync(RequestBuilder requestBuilder, } /// - /// + /// Handles calling the API for requests that are expected to return no content + /// + /// + /// + /// + /// + /// + public async Task SendRequestAsync(RequestBuilder requestBuilder, + IDictionary? additionalHeaders = null, + string? apiName = null, CancellationToken cancellationToken = default) { + var request = requestBuilder.BuildRequest(); + + await this.SendRequestAsync(request, additionalHeaders, apiName, cancellationToken); + } + + /// + /// Handles calling the API /// /// /// @@ -102,6 +118,35 @@ public async Task SendRequestAsync(HttpRequestMessage request, } } + /// + /// Handles calling the API for requests that are expected to return no content + /// + /// + /// + /// + /// + /// + /// + /// + public async Task SendRequestAsync(HttpRequestMessage request, + IDictionary? additionalHeaders = null, + string? apiName = null, CancellationToken cancellationToken = default) { + if (additionalHeaders != null) { + foreach (var header in additionalHeaders) { + if (header.Value != null) { + request.Headers.Add(header.Key, header.Value); + } + } + } + + var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false); + { + if (!response.IsSuccessStatusCode) { + throw await ApiException.CreateSpecificExceptionAsync(response, request, apiName).ConfigureAwait(false); + } + } + } + /// /// Disposes of any owned disposable resources such as the underlying if owned. /// diff --git a/src/Com.Auth0.FGA/Com.Auth0.FGA.csproj b/src/Com.Auth0.FGA/Com.Auth0.FGA.csproj index 428b764..e89ff08 100644 --- a/src/Com.Auth0.FGA/Com.Auth0.FGA.csproj +++ b/src/Com.Auth0.FGA/Com.Auth0.FGA.csproj @@ -12,7 +12,7 @@ .NET SDK for Auth0 Fine Grained Authorization (FGA) Auth0® Inc. Com.Auth0.FGA - 0.1.0 + 0.1.1 bin\$(Configuration)\$(TargetFramework)\Com.Auth0.FGA.xml MIT https://github.com/auth0-lab/fga-dotnet-sdk.git diff --git a/src/Com.Auth0.FGA/Configuration/Configuration.cs b/src/Com.Auth0.FGA/Configuration/Configuration.cs index e4e113c..e864877 100644 --- a/src/Com.Auth0.FGA/Configuration/Configuration.cs +++ b/src/Com.Auth0.FGA/Configuration/Configuration.cs @@ -59,7 +59,7 @@ public void IsValid() { /// Version of the package. /// /// Version of the package. - public const string Version = "0.1.0"; + public const string Version = "0.1.1"; #endregion Constants @@ -84,7 +84,7 @@ public Configuration() { if (string.IsNullOrEmpty(Environment)) { Environment = DefaultEnvironment; } - UserAgent = "auth0-fga-sdk (dotnet) 0.1.0"; + UserAgent = "auth0-fga-sdk (dotnet) 0.1.1"; DefaultHeaders ??= new Dictionary(); if (!DefaultHeaders.ContainsKey("User-Agent")) {