From 5deabb88fcedff97b8108416cd3313ee1711e8a6 Mon Sep 17 00:00:00 2001 From: "Justin \"J.R.\" Hill" Date: Fri, 8 Sep 2023 23:25:10 +0900 Subject: [PATCH] feat(java): Introduce OAuth2Client (#11) --- .openapi-generator-ignore | 8 +- .openapi-generator/FILES | 23 ++- .../java/dev/openfga/sdk/api/OpenFgaApi.java | 193 +++++++++++++----- .../dev/openfga/sdk/api/auth/AccessToken.java | 49 +++++ .../sdk/api/auth/CredentialsFlowRequest.java | 85 ++++++++ .../sdk/api/auth/CredentialsFlowResponse.java | 88 ++++++++ .../openfga/sdk/api/auth/OAuth2Client.java | 106 ++++++++++ .../dev/openfga/sdk/api/client/ApiClient.java | 40 ++++ .../sdk/api/client/CredentialsMethod.java | 41 ---- .../sdk/api/client/RFC3339DateFormat.java | 55 ----- .../ApiToken.java} | 10 +- .../ClientCredentials.java | 4 +- .../sdk/api/configuration/Configuration.java | 42 +++- .../configuration/ConfigurationOverride.java | 22 ++ .../sdk/api/configuration/Credentials.java | 69 +++++++ .../api/configuration/CredentialsMethod.java | 37 ++++ .../{api/client => errors}/ApiException.java | 18 +- .../sdk/{api/client => util}/Pair.java | 2 +- .../openfga/{ => sdk}/util/StringUtil.java | 2 +- .../sdk/api/OpenFgaApiIntegrationTest.java | 4 +- .../dev/openfga/sdk/api/OpenFgaApiTest.java | 3 +- .../sdk/api/auth/OAuth2ClientTest.java | 57 ++++++ .../ClientCredentialsTest.java | 2 +- .../{ => sdk}/util/StringUtilTest.java | 4 +- 24 files changed, 795 insertions(+), 169 deletions(-) create mode 100644 src/main/java/dev/openfga/sdk/api/auth/AccessToken.java create mode 100644 src/main/java/dev/openfga/sdk/api/auth/CredentialsFlowRequest.java create mode 100644 src/main/java/dev/openfga/sdk/api/auth/CredentialsFlowResponse.java create mode 100644 src/main/java/dev/openfga/sdk/api/auth/OAuth2Client.java delete mode 100644 src/main/java/dev/openfga/sdk/api/client/CredentialsMethod.java delete mode 100644 src/main/java/dev/openfga/sdk/api/client/RFC3339DateFormat.java rename src/main/java/dev/openfga/sdk/api/{client/ApiBearerToken.java => configuration/ApiToken.java} (71%) rename src/main/java/dev/openfga/sdk/api/{client => configuration}/ClientCredentials.java (95%) create mode 100644 src/main/java/dev/openfga/sdk/api/configuration/Credentials.java create mode 100644 src/main/java/dev/openfga/sdk/api/configuration/CredentialsMethod.java rename src/main/java/dev/openfga/sdk/{api/client => errors}/ApiException.java (79%) rename src/main/java/dev/openfga/sdk/{api/client => util}/Pair.java (96%) rename src/main/java/dev/openfga/{ => sdk}/util/StringUtil.java (97%) create mode 100644 src/test/java/dev/openfga/sdk/api/auth/OAuth2ClientTest.java rename src/test/java/dev/openfga/sdk/api/{client => configuration}/ClientCredentialsTest.java (99%) rename src/test/java/dev/openfga/{ => sdk}/util/StringUtilTest.java (92%) diff --git a/.openapi-generator-ignore b/.openapi-generator-ignore index 228aa29..e3549f0 100644 --- a/.openapi-generator-ignore +++ b/.openapi-generator-ignore @@ -1,7 +1,13 @@ +# Used but at a different location +**/client/Configuration.java +**/client/Pair.java +**/client/ApiException.java + +# Unused **/ServerConfiguration.java **/ServerVariable.java **/JSON.java -**/client/Configuration.java +**/RFC3339DateFormat.java src/main/AndroidManifest.xml build.sbt pom.xml diff --git a/.openapi-generator/FILES b/.openapi-generator/FILES index e931a85..a810c10 100644 --- a/.openapi-generator/FILES +++ b/.openapi-generator/FILES @@ -75,17 +75,19 @@ gradlew gradlew.bat settings.gradle src/main/java/dev/openfga/sdk/api/OpenFgaApi.java -src/main/java/dev/openfga/sdk/api/client/ApiBearerToken.java +src/main/java/dev/openfga/sdk/api/auth/AccessToken.java +src/main/java/dev/openfga/sdk/api/auth/CredentialsFlowRequest.java +src/main/java/dev/openfga/sdk/api/auth/CredentialsFlowResponse.java +src/main/java/dev/openfga/sdk/api/auth/OAuth2Client.java src/main/java/dev/openfga/sdk/api/client/ApiClient.java -src/main/java/dev/openfga/sdk/api/client/ApiException.java src/main/java/dev/openfga/sdk/api/client/ApiResponse.java -src/main/java/dev/openfga/sdk/api/client/ClientCredentials.java -src/main/java/dev/openfga/sdk/api/client/CredentialsMethod.java -src/main/java/dev/openfga/sdk/api/client/Pair.java -src/main/java/dev/openfga/sdk/api/client/RFC3339DateFormat.java +src/main/java/dev/openfga/sdk/api/configuration/ApiToken.java src/main/java/dev/openfga/sdk/api/configuration/BaseConfiguration.java +src/main/java/dev/openfga/sdk/api/configuration/ClientCredentials.java src/main/java/dev/openfga/sdk/api/configuration/Configuration.java src/main/java/dev/openfga/sdk/api/configuration/ConfigurationOverride.java +src/main/java/dev/openfga/sdk/api/configuration/Credentials.java +src/main/java/dev/openfga/sdk/api/configuration/CredentialsMethod.java src/main/java/dev/openfga/sdk/api/model/AbstractOpenApiSchema.java src/main/java/dev/openfga/sdk/api/model/Any.java src/main/java/dev/openfga/sdk/api/model/Assertion.java @@ -141,12 +143,15 @@ src/main/java/dev/openfga/sdk/api/model/WriteAssertionsRequest.java src/main/java/dev/openfga/sdk/api/model/WriteAuthorizationModelRequest.java src/main/java/dev/openfga/sdk/api/model/WriteAuthorizationModelResponse.java src/main/java/dev/openfga/sdk/api/model/WriteRequest.java +src/main/java/dev/openfga/sdk/errors/ApiException.java src/main/java/dev/openfga/sdk/errors/FgaInvalidParameterException.java -src/main/java/dev/openfga/util/StringUtil.java +src/main/java/dev/openfga/sdk/util/Pair.java +src/main/java/dev/openfga/sdk/util/StringUtil.java src/test-integration/java/dev/openfga/sdk/api/OpenFgaApiIntegrationTest.java src/test/java/dev/openfga/sdk/api/OpenFgaApiTest.java src/test/java/dev/openfga/sdk/api/OpenFgaApiTest.java -src/test/java/dev/openfga/sdk/api/client/ClientCredentialsTest.java +src/test/java/dev/openfga/sdk/api/auth/OAuth2ClientTest.java +src/test/java/dev/openfga/sdk/api/configuration/ClientCredentialsTest.java src/test/java/dev/openfga/sdk/api/configuration/ConfigurationTest.java src/test/java/dev/openfga/sdk/api/model/AnyTest.java src/test/java/dev/openfga/sdk/api/model/AssertionTest.java @@ -202,4 +207,4 @@ src/test/java/dev/openfga/sdk/api/model/WriteAssertionsRequestTest.java src/test/java/dev/openfga/sdk/api/model/WriteAuthorizationModelRequestTest.java src/test/java/dev/openfga/sdk/api/model/WriteAuthorizationModelResponseTest.java src/test/java/dev/openfga/sdk/api/model/WriteRequestTest.java -src/test/java/dev/openfga/util/StringUtilTest.java +src/test/java/dev/openfga/sdk/util/StringUtilTest.java diff --git a/src/main/java/dev/openfga/sdk/api/OpenFgaApi.java b/src/main/java/dev/openfga/sdk/api/OpenFgaApi.java index 158a37f..1b27e63 100644 --- a/src/main/java/dev/openfga/sdk/api/OpenFgaApi.java +++ b/src/main/java/dev/openfga/sdk/api/OpenFgaApi.java @@ -14,12 +14,9 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; -import dev.openfga.sdk.api.client.ApiClient; -import dev.openfga.sdk.api.client.ApiException; -import dev.openfga.sdk.api.client.ApiResponse; -import dev.openfga.sdk.api.client.Pair; -import dev.openfga.sdk.api.configuration.Configuration; -import dev.openfga.sdk.api.configuration.ConfigurationOverride; +import dev.openfga.sdk.api.auth.*; +import dev.openfga.sdk.api.client.*; +import dev.openfga.sdk.api.configuration.*; import dev.openfga.sdk.api.model.CheckRequest; import dev.openfga.sdk.api.model.CheckResponse; import dev.openfga.sdk.api.model.CreateStoreRequest; @@ -40,7 +37,8 @@ import dev.openfga.sdk.api.model.WriteAuthorizationModelRequest; import dev.openfga.sdk.api.model.WriteAuthorizationModelResponse; import dev.openfga.sdk.api.model.WriteRequest; -import dev.openfga.sdk.errors.FgaInvalidParameterException; +import dev.openfga.sdk.errors.*; +import dev.openfga.sdk.util.Pair; import java.io.IOException; import java.io.InputStream; import java.net.URI; @@ -58,29 +56,26 @@ public class OpenFgaApi { private final HttpClient memberVarHttpClient; private final ObjectMapper memberVarObjectMapper; private final Configuration configuration; + private final OAuth2Client oAuth2Client; private final Consumer memberVarInterceptor; private final Consumer> memberVarResponseInterceptor; private final Consumer> memberVarAsyncResponseInterceptor; - public OpenFgaApi(ApiClient apiClient, Configuration configuration) { + // TODO: In every request, get access token, (Assuming plain access token, or OAuth2 CredentialsMethod) + + public OpenFgaApi(ApiClient apiClient, Configuration configuration) throws FgaInvalidParameterException { memberVarHttpClient = apiClient.getHttpClient(); memberVarObjectMapper = apiClient.getObjectMapper(); this.configuration = configuration; memberVarInterceptor = apiClient.getRequestInterceptor(); memberVarResponseInterceptor = apiClient.getResponseInterceptor(); memberVarAsyncResponseInterceptor = apiClient.getAsyncResponseInterceptor(); - } - - private ApiException getApiException(String operationId, HttpResponse response) { - String message = formatExceptionMessage(operationId, response.statusCode(), response.body()); - return new ApiException(response.statusCode(), message, response.headers(), response.body()); - } - private String formatExceptionMessage(String operationId, int statusCode, String body) { - if (body == null || body.isEmpty()) { - body = "[no body]"; + if (configuration.getCredentials().getCredentialsMethod() == CredentialsMethod.CLIENT_CREDENTIALS) { + this.oAuth2Client = new OAuth2Client(configuration, apiClient.getHttpClient(), apiClient.getObjectMapper()); + } else { + this.oAuth2Client = null; } - return operationId + " call failed with: " + statusCode + " - " + body; } /** @@ -119,7 +114,7 @@ private CompletableFuture check(String storeId, CheckRequest body .sendAsync(localVarRequestBuilder.build(), HttpResponse.BodyHandlers.ofString()) .thenComposeAsync(localVarResponse -> { if (localVarResponse.statusCode() / 100 != 2) { - return CompletableFuture.failedFuture(getApiException("check", localVarResponse)); + return CompletableFuture.failedFuture(new ApiException("check", localVarResponse)); } try { String responseBody = localVarResponse.body(); @@ -177,7 +172,7 @@ private CompletableFuture> checkWithHttpInfo( memberVarAsyncResponseInterceptor.accept(localVarResponse); } if (localVarResponse.statusCode() / 100 != 2) { - return CompletableFuture.failedFuture(getApiException("check", localVarResponse)); + return CompletableFuture.failedFuture(new ApiException("check", localVarResponse)); } try { String responseBody = localVarResponse.body(); @@ -220,6 +215,11 @@ private HttpRequest.Builder checkRequestBuilder(String storeId, CheckRequest bod localVarRequestBuilder.header("Content-Type", "application/json"); localVarRequestBuilder.header("Accept", "application/json"); + if (configuration.getCredentials().getCredentialsMethod() != CredentialsMethod.NONE) { + String accessToken = getAccessToken(configuration); + localVarRequestBuilder.header("Authorization", "Bearer " + accessToken); + } + try { byte[] localVarPostBody = memberVarObjectMapper.writeValueAsBytes(body); localVarRequestBuilder.method("POST", HttpRequest.BodyPublishers.ofByteArray(localVarPostBody)); @@ -270,7 +270,7 @@ private CompletableFuture createStore(CreateStoreRequest bo .sendAsync(localVarRequestBuilder.build(), HttpResponse.BodyHandlers.ofString()) .thenComposeAsync(localVarResponse -> { if (localVarResponse.statusCode() / 100 != 2) { - return CompletableFuture.failedFuture(getApiException("createStore", localVarResponse)); + return CompletableFuture.failedFuture(new ApiException("createStore", localVarResponse)); } try { String responseBody = localVarResponse.body(); @@ -325,7 +325,7 @@ private CompletableFuture> createStoreWithHttpI memberVarAsyncResponseInterceptor.accept(localVarResponse); } if (localVarResponse.statusCode() / 100 != 2) { - return CompletableFuture.failedFuture(getApiException("createStore", localVarResponse)); + return CompletableFuture.failedFuture(new ApiException("createStore", localVarResponse)); } try { String responseBody = localVarResponse.body(); @@ -364,6 +364,11 @@ private HttpRequest.Builder createStoreRequestBuilder(CreateStoreRequest body, C localVarRequestBuilder.header("Content-Type", "application/json"); localVarRequestBuilder.header("Accept", "application/json"); + if (configuration.getCredentials().getCredentialsMethod() != CredentialsMethod.NONE) { + String accessToken = getAccessToken(configuration); + localVarRequestBuilder.header("Authorization", "Bearer " + accessToken); + } + try { byte[] localVarPostBody = memberVarObjectMapper.writeValueAsBytes(body); localVarRequestBuilder.method("POST", HttpRequest.BodyPublishers.ofByteArray(localVarPostBody)); @@ -412,7 +417,7 @@ private CompletableFuture deleteStore(String storeId, Configuration config .sendAsync(localVarRequestBuilder.build(), HttpResponse.BodyHandlers.ofString()) .thenComposeAsync(localVarResponse -> { if (localVarResponse.statusCode() / 100 != 2) { - return CompletableFuture.failedFuture(getApiException("deleteStore", localVarResponse)); + return CompletableFuture.failedFuture(new ApiException("deleteStore", localVarResponse)); } return CompletableFuture.completedFuture(null); }); @@ -458,7 +463,7 @@ private CompletableFuture> deleteStoreWithHttpInfo(String stor memberVarAsyncResponseInterceptor.accept(localVarResponse); } if (localVarResponse.statusCode() / 100 != 2) { - return CompletableFuture.failedFuture(getApiException("deleteStore", localVarResponse)); + return CompletableFuture.failedFuture(new ApiException("deleteStore", localVarResponse)); } return CompletableFuture.completedFuture(new ApiResponse( localVarResponse.statusCode(), @@ -488,6 +493,11 @@ private HttpRequest.Builder deleteStoreRequestBuilder(String storeId, Configurat localVarRequestBuilder.header("Accept", "application/json"); + if (configuration.getCredentials().getCredentialsMethod() != CredentialsMethod.NONE) { + String accessToken = getAccessToken(configuration); + localVarRequestBuilder.header("Authorization", "Bearer " + accessToken); + } + localVarRequestBuilder.method("DELETE", HttpRequest.BodyPublishers.noBody()); Duration readTimeout = configuration.getReadTimeout(); if (readTimeout != null) { @@ -535,7 +545,7 @@ private CompletableFuture expand(String storeId, ExpandRequest b .sendAsync(localVarRequestBuilder.build(), HttpResponse.BodyHandlers.ofString()) .thenComposeAsync(localVarResponse -> { if (localVarResponse.statusCode() / 100 != 2) { - return CompletableFuture.failedFuture(getApiException("expand", localVarResponse)); + return CompletableFuture.failedFuture(new ApiException("expand", localVarResponse)); } try { String responseBody = localVarResponse.body(); @@ -593,7 +603,7 @@ private CompletableFuture> expandWithHttpInfo( memberVarAsyncResponseInterceptor.accept(localVarResponse); } if (localVarResponse.statusCode() / 100 != 2) { - return CompletableFuture.failedFuture(getApiException("expand", localVarResponse)); + return CompletableFuture.failedFuture(new ApiException("expand", localVarResponse)); } try { String responseBody = localVarResponse.body(); @@ -637,6 +647,11 @@ private HttpRequest.Builder expandRequestBuilder(String storeId, ExpandRequest b localVarRequestBuilder.header("Content-Type", "application/json"); localVarRequestBuilder.header("Accept", "application/json"); + if (configuration.getCredentials().getCredentialsMethod() != CredentialsMethod.NONE) { + String accessToken = getAccessToken(configuration); + localVarRequestBuilder.header("Authorization", "Bearer " + accessToken); + } + try { byte[] localVarPostBody = memberVarObjectMapper.writeValueAsBytes(body); localVarRequestBuilder.method("POST", HttpRequest.BodyPublishers.ofByteArray(localVarPostBody)); @@ -686,7 +701,7 @@ private CompletableFuture getStore(String storeId, Configurati .sendAsync(localVarRequestBuilder.build(), HttpResponse.BodyHandlers.ofString()) .thenComposeAsync(localVarResponse -> { if (localVarResponse.statusCode() / 100 != 2) { - return CompletableFuture.failedFuture(getApiException("getStore", localVarResponse)); + return CompletableFuture.failedFuture(new ApiException("getStore", localVarResponse)); } try { String responseBody = localVarResponse.body(); @@ -741,7 +756,7 @@ private CompletableFuture> getStoreWithHttpInfo( memberVarAsyncResponseInterceptor.accept(localVarResponse); } if (localVarResponse.statusCode() / 100 != 2) { - return CompletableFuture.failedFuture(getApiException("getStore", localVarResponse)); + return CompletableFuture.failedFuture(new ApiException("getStore", localVarResponse)); } try { String responseBody = localVarResponse.body(); @@ -779,6 +794,11 @@ private HttpRequest.Builder getStoreRequestBuilder(String storeId, Configuration localVarRequestBuilder.header("Accept", "application/json"); + if (configuration.getCredentials().getCredentialsMethod() != CredentialsMethod.NONE) { + String accessToken = getAccessToken(configuration); + localVarRequestBuilder.header("Authorization", "Bearer " + accessToken); + } + localVarRequestBuilder.method("GET", HttpRequest.BodyPublishers.noBody()); Duration readTimeout = configuration.getReadTimeout(); if (readTimeout != null) { @@ -827,7 +847,7 @@ private CompletableFuture listObjects( .sendAsync(localVarRequestBuilder.build(), HttpResponse.BodyHandlers.ofString()) .thenComposeAsync(localVarResponse -> { if (localVarResponse.statusCode() / 100 != 2) { - return CompletableFuture.failedFuture(getApiException("listObjects", localVarResponse)); + return CompletableFuture.failedFuture(new ApiException("listObjects", localVarResponse)); } try { String responseBody = localVarResponse.body(); @@ -885,7 +905,7 @@ private CompletableFuture> listObjectsWithHttpI memberVarAsyncResponseInterceptor.accept(localVarResponse); } if (localVarResponse.statusCode() / 100 != 2) { - return CompletableFuture.failedFuture(getApiException("listObjects", localVarResponse)); + return CompletableFuture.failedFuture(new ApiException("listObjects", localVarResponse)); } try { String responseBody = localVarResponse.body(); @@ -930,6 +950,11 @@ private HttpRequest.Builder listObjectsRequestBuilder( localVarRequestBuilder.header("Content-Type", "application/json"); localVarRequestBuilder.header("Accept", "application/json"); + if (configuration.getCredentials().getCredentialsMethod() != CredentialsMethod.NONE) { + String accessToken = getAccessToken(configuration); + localVarRequestBuilder.header("Authorization", "Bearer " + accessToken); + } + try { byte[] localVarPostBody = memberVarObjectMapper.writeValueAsBytes(body); localVarRequestBuilder.method("POST", HttpRequest.BodyPublishers.ofByteArray(localVarPostBody)); @@ -984,7 +1009,7 @@ private CompletableFuture listStores( .sendAsync(localVarRequestBuilder.build(), HttpResponse.BodyHandlers.ofString()) .thenComposeAsync(localVarResponse -> { if (localVarResponse.statusCode() / 100 != 2) { - return CompletableFuture.failedFuture(getApiException("listStores", localVarResponse)); + return CompletableFuture.failedFuture(new ApiException("listStores", localVarResponse)); } try { String responseBody = localVarResponse.body(); @@ -1043,7 +1068,7 @@ private CompletableFuture> listStoresWithHttpInf memberVarAsyncResponseInterceptor.accept(localVarResponse); } if (localVarResponse.statusCode() / 100 != 2) { - return CompletableFuture.failedFuture(getApiException("listStores", localVarResponse)); + return CompletableFuture.failedFuture(new ApiException("listStores", localVarResponse)); } try { String responseBody = localVarResponse.body(); @@ -1096,6 +1121,11 @@ private HttpRequest.Builder listStoresRequestBuilder( localVarRequestBuilder.header("Accept", "application/json"); + if (configuration.getCredentials().getCredentialsMethod() != CredentialsMethod.NONE) { + String accessToken = getAccessToken(configuration); + localVarRequestBuilder.header("Authorization", "Bearer " + accessToken); + } + localVarRequestBuilder.method("GET", HttpRequest.BodyPublishers.noBody()); Duration readTimeout = configuration.getReadTimeout(); if (readTimeout != null) { @@ -1143,7 +1173,7 @@ private CompletableFuture read(String storeId, ReadRequest body, C .sendAsync(localVarRequestBuilder.build(), HttpResponse.BodyHandlers.ofString()) .thenComposeAsync(localVarResponse -> { if (localVarResponse.statusCode() / 100 != 2) { - return CompletableFuture.failedFuture(getApiException("read", localVarResponse)); + return CompletableFuture.failedFuture(new ApiException("read", localVarResponse)); } try { String responseBody = localVarResponse.body(); @@ -1201,7 +1231,7 @@ private CompletableFuture> readWithHttpInfo( memberVarAsyncResponseInterceptor.accept(localVarResponse); } if (localVarResponse.statusCode() / 100 != 2) { - return CompletableFuture.failedFuture(getApiException("read", localVarResponse)); + return CompletableFuture.failedFuture(new ApiException("read", localVarResponse)); } try { String responseBody = localVarResponse.body(); @@ -1244,6 +1274,11 @@ private HttpRequest.Builder readRequestBuilder(String storeId, ReadRequest body, localVarRequestBuilder.header("Content-Type", "application/json"); localVarRequestBuilder.header("Accept", "application/json"); + if (configuration.getCredentials().getCredentialsMethod() != CredentialsMethod.NONE) { + String accessToken = getAccessToken(configuration); + localVarRequestBuilder.header("Authorization", "Bearer " + accessToken); + } + try { byte[] localVarPostBody = memberVarObjectMapper.writeValueAsBytes(body); localVarRequestBuilder.method("POST", HttpRequest.BodyPublishers.ofByteArray(localVarPostBody)); @@ -1298,7 +1333,7 @@ private CompletableFuture readAssertions( .sendAsync(localVarRequestBuilder.build(), HttpResponse.BodyHandlers.ofString()) .thenComposeAsync(localVarResponse -> { if (localVarResponse.statusCode() / 100 != 2) { - return CompletableFuture.failedFuture(getApiException("readAssertions", localVarResponse)); + return CompletableFuture.failedFuture(new ApiException("readAssertions", localVarResponse)); } try { String responseBody = localVarResponse.body(); @@ -1358,7 +1393,7 @@ private CompletableFuture> readAssertionsWit memberVarAsyncResponseInterceptor.accept(localVarResponse); } if (localVarResponse.statusCode() / 100 != 2) { - return CompletableFuture.failedFuture(getApiException("readAssertions", localVarResponse)); + return CompletableFuture.failedFuture(new ApiException("readAssertions", localVarResponse)); } try { String responseBody = localVarResponse.body(); @@ -1404,6 +1439,11 @@ private HttpRequest.Builder readAssertionsRequestBuilder( localVarRequestBuilder.header("Accept", "application/json"); + if (configuration.getCredentials().getCredentialsMethod() != CredentialsMethod.NONE) { + String accessToken = getAccessToken(configuration); + localVarRequestBuilder.header("Authorization", "Bearer " + accessToken); + } + localVarRequestBuilder.method("GET", HttpRequest.BodyPublishers.noBody()); Duration readTimeout = configuration.getReadTimeout(); if (readTimeout != null) { @@ -1453,7 +1493,7 @@ private CompletableFuture readAuthorizationModel .thenComposeAsync(localVarResponse -> { if (localVarResponse.statusCode() / 100 != 2) { return CompletableFuture.failedFuture( - getApiException("readAuthorizationModel", localVarResponse)); + new ApiException("readAuthorizationModel", localVarResponse)); } try { String responseBody = localVarResponse.body(); @@ -1513,7 +1553,7 @@ private CompletableFuture> readAutho } if (localVarResponse.statusCode() / 100 != 2) { return CompletableFuture.failedFuture( - getApiException("readAuthorizationModel", localVarResponse)); + new ApiException("readAuthorizationModel", localVarResponse)); } try { String responseBody = localVarResponse.body(); @@ -1558,6 +1598,11 @@ private HttpRequest.Builder readAuthorizationModelRequestBuilder( localVarRequestBuilder.header("Accept", "application/json"); + if (configuration.getCredentials().getCredentialsMethod() != CredentialsMethod.NONE) { + String accessToken = getAccessToken(configuration); + localVarRequestBuilder.header("Authorization", "Bearer " + accessToken); + } + localVarRequestBuilder.method("GET", HttpRequest.BodyPublishers.noBody()); Duration readTimeout = configuration.getReadTimeout(); if (readTimeout != null) { @@ -1612,7 +1657,7 @@ private CompletableFuture readAuthorizationMode .thenComposeAsync(localVarResponse -> { if (localVarResponse.statusCode() / 100 != 2) { return CompletableFuture.failedFuture( - getApiException("readAuthorizationModels", localVarResponse)); + new ApiException("readAuthorizationModels", localVarResponse)); } try { String responseBody = localVarResponse.body(); @@ -1677,7 +1722,7 @@ private CompletableFuture> readAuth } if (localVarResponse.statusCode() / 100 != 2) { return CompletableFuture.failedFuture( - getApiException("readAuthorizationModels", localVarResponse)); + new ApiException("readAuthorizationModels", localVarResponse)); } try { String responseBody = localVarResponse.body(); @@ -1737,6 +1782,11 @@ private HttpRequest.Builder readAuthorizationModelsRequestBuilder( localVarRequestBuilder.header("Accept", "application/json"); + if (configuration.getCredentials().getCredentialsMethod() != CredentialsMethod.NONE) { + String accessToken = getAccessToken(configuration); + localVarRequestBuilder.header("Authorization", "Bearer " + accessToken); + } + localVarRequestBuilder.method("GET", HttpRequest.BodyPublishers.noBody()); Duration readTimeout = configuration.getReadTimeout(); if (readTimeout != null) { @@ -1796,7 +1846,7 @@ private CompletableFuture readChanges( .sendAsync(localVarRequestBuilder.build(), HttpResponse.BodyHandlers.ofString()) .thenComposeAsync(localVarResponse -> { if (localVarResponse.statusCode() / 100 != 2) { - return CompletableFuture.failedFuture(getApiException("readChanges", localVarResponse)); + return CompletableFuture.failedFuture(new ApiException("readChanges", localVarResponse)); } try { String responseBody = localVarResponse.body(); @@ -1865,7 +1915,7 @@ private CompletableFuture> readChangesWithHttpI memberVarAsyncResponseInterceptor.accept(localVarResponse); } if (localVarResponse.statusCode() / 100 != 2) { - return CompletableFuture.failedFuture(getApiException("readChanges", localVarResponse)); + return CompletableFuture.failedFuture(new ApiException("readChanges", localVarResponse)); } try { String responseBody = localVarResponse.body(); @@ -1925,6 +1975,11 @@ private HttpRequest.Builder readChangesRequestBuilder( localVarRequestBuilder.header("Accept", "application/json"); + if (configuration.getCredentials().getCredentialsMethod() != CredentialsMethod.NONE) { + String accessToken = getAccessToken(configuration); + localVarRequestBuilder.header("Authorization", "Bearer " + accessToken); + } + localVarRequestBuilder.method("GET", HttpRequest.BodyPublishers.noBody()); Duration readTimeout = configuration.getReadTimeout(); if (readTimeout != null) { @@ -1972,7 +2027,7 @@ private CompletableFuture write(String storeId, WriteRequest body, Confi .sendAsync(localVarRequestBuilder.build(), HttpResponse.BodyHandlers.ofString()) .thenComposeAsync(localVarResponse -> { if (localVarResponse.statusCode() / 100 != 2) { - return CompletableFuture.failedFuture(getApiException("write", localVarResponse)); + return CompletableFuture.failedFuture(new ApiException("write", localVarResponse)); } try { String responseBody = localVarResponse.body(); @@ -2030,7 +2085,7 @@ private CompletableFuture> writeWithHttpInfo( memberVarAsyncResponseInterceptor.accept(localVarResponse); } if (localVarResponse.statusCode() / 100 != 2) { - return CompletableFuture.failedFuture(getApiException("write", localVarResponse)); + return CompletableFuture.failedFuture(new ApiException("write", localVarResponse)); } try { String responseBody = localVarResponse.body(); @@ -2073,6 +2128,11 @@ private HttpRequest.Builder writeRequestBuilder(String storeId, WriteRequest bod localVarRequestBuilder.header("Content-Type", "application/json"); localVarRequestBuilder.header("Accept", "application/json"); + if (configuration.getCredentials().getCredentialsMethod() != CredentialsMethod.NONE) { + String accessToken = getAccessToken(configuration); + localVarRequestBuilder.header("Authorization", "Bearer " + accessToken); + } + try { byte[] localVarPostBody = memberVarObjectMapper.writeValueAsBytes(body); localVarRequestBuilder.method("POST", HttpRequest.BodyPublishers.ofByteArray(localVarPostBody)); @@ -2133,7 +2193,8 @@ private CompletableFuture writeAssertions( .sendAsync(localVarRequestBuilder.build(), HttpResponse.BodyHandlers.ofString()) .thenComposeAsync(localVarResponse -> { if (localVarResponse.statusCode() / 100 != 2) { - return CompletableFuture.failedFuture(getApiException("writeAssertions", localVarResponse)); + return CompletableFuture.failedFuture( + new ApiException("writeAssertions", localVarResponse)); } return CompletableFuture.completedFuture(null); }); @@ -2190,7 +2251,8 @@ private CompletableFuture> writeAssertionsWithHttpInfo( memberVarAsyncResponseInterceptor.accept(localVarResponse); } if (localVarResponse.statusCode() / 100 != 2) { - return CompletableFuture.failedFuture(getApiException("writeAssertions", localVarResponse)); + return CompletableFuture.failedFuture( + new ApiException("writeAssertions", localVarResponse)); } return CompletableFuture.completedFuture(new ApiResponse( localVarResponse.statusCode(), @@ -2233,6 +2295,11 @@ private HttpRequest.Builder writeAssertionsRequestBuilder( localVarRequestBuilder.header("Content-Type", "application/json"); localVarRequestBuilder.header("Accept", "application/json"); + if (configuration.getCredentials().getCredentialsMethod() != CredentialsMethod.NONE) { + String accessToken = getAccessToken(configuration); + localVarRequestBuilder.header("Authorization", "Bearer " + accessToken); + } + try { byte[] localVarPostBody = memberVarObjectMapper.writeValueAsBytes(body); localVarRequestBuilder.method("PUT", HttpRequest.BodyPublishers.ofByteArray(localVarPostBody)); @@ -2288,7 +2355,7 @@ private CompletableFuture writeAuthorizationMod .thenComposeAsync(localVarResponse -> { if (localVarResponse.statusCode() / 100 != 2) { return CompletableFuture.failedFuture( - getApiException("writeAuthorizationModel", localVarResponse)); + new ApiException("writeAuthorizationModel", localVarResponse)); } try { String responseBody = localVarResponse.body(); @@ -2349,7 +2416,7 @@ private CompletableFuture> writeAut } if (localVarResponse.statusCode() / 100 != 2) { return CompletableFuture.failedFuture( - getApiException("writeAuthorizationModel", localVarResponse)); + new ApiException("writeAuthorizationModel", localVarResponse)); } try { String responseBody = localVarResponse.body(); @@ -2396,6 +2463,11 @@ private HttpRequest.Builder writeAuthorizationModelRequestBuilder( localVarRequestBuilder.header("Content-Type", "application/json"); localVarRequestBuilder.header("Accept", "application/json"); + if (configuration.getCredentials().getCredentialsMethod() != CredentialsMethod.NONE) { + String accessToken = getAccessToken(configuration); + localVarRequestBuilder.header("Authorization", "Bearer " + accessToken); + } + try { byte[] localVarPostBody = memberVarObjectMapper.writeValueAsBytes(body); localVarRequestBuilder.method("POST", HttpRequest.BodyPublishers.ofByteArray(localVarPostBody)); @@ -2411,4 +2483,29 @@ private HttpRequest.Builder writeAuthorizationModelRequestBuilder( } return localVarRequestBuilder; } + + /** + * Get an access token. Expects that configuration is valid (meaning it can + * pass {@link Configuration#assertValid()}) and expects that if the + * CredentialsMethod is CLIENT_CREDENTIALS that a valid {@link OAuth2Client} + * has been initialized. Otherwise, it will throw an IllegalStateException. + * @throws IllegalStateException when the configuration is invalid + */ + private String getAccessToken(Configuration configuration) throws ApiException { + CredentialsMethod credentialsMethod = configuration.getCredentials().getCredentialsMethod(); + + if (credentialsMethod == CredentialsMethod.API_TOKEN) { + return configuration.getCredentials().getApiToken().getToken(); + } + + if (credentialsMethod == CredentialsMethod.CLIENT_CREDENTIALS) { + try { + return oAuth2Client.getAccessToken().get(); + } catch (Exception e) { + throw new ApiException(e); + } + } + + throw new IllegalStateException("Configuration is invalid."); + } } diff --git a/src/main/java/dev/openfga/sdk/api/auth/AccessToken.java b/src/main/java/dev/openfga/sdk/api/auth/AccessToken.java new file mode 100644 index 0000000..1116c61 --- /dev/null +++ b/src/main/java/dev/openfga/sdk/api/auth/AccessToken.java @@ -0,0 +1,49 @@ +/* + * OpenFGA + * A high performance and flexible authorization/permission engine built for developers and inspired by Google Zanzibar. + * + * The version of the OpenAPI document: 0.1 + * Contact: community@openfga.dev + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +package dev.openfga.sdk.api.auth; + +import static dev.openfga.sdk.util.StringUtil.isNullOrWhitespace; + +import java.time.Instant; +import java.util.Random; + +class AccessToken { + private static final int TOKEN_EXPIRY_BUFFER_THRESHOLD_IN_SEC = 300; + private static final int TOKEN_EXPIRY_JITTER_IN_SEC = + 300; // We add some jitter so that token refreshes are less likely to collide + + private final Random random = new Random(); + private Instant expiresAt; + + private String token; + + public boolean isValid() { + return !isNullOrWhitespace(token) + && (expiresAt == null + || expiresAt.isBefore(Instant.now() + .plusSeconds(TOKEN_EXPIRY_BUFFER_THRESHOLD_IN_SEC) + .plusSeconds(random.nextLong() % TOKEN_EXPIRY_JITTER_IN_SEC))); + } + + public String getToken() { + return token; + } + + public void setExpiresAt(Instant expiresAt) { + this.expiresAt = expiresAt; + } + + public void setToken(String token) { + this.token = token; + } +} diff --git a/src/main/java/dev/openfga/sdk/api/auth/CredentialsFlowRequest.java b/src/main/java/dev/openfga/sdk/api/auth/CredentialsFlowRequest.java new file mode 100644 index 0000000..f9f14ca --- /dev/null +++ b/src/main/java/dev/openfga/sdk/api/auth/CredentialsFlowRequest.java @@ -0,0 +1,85 @@ +/* + * OpenFGA + * A high performance and flexible authorization/permission engine built for developers and inspired by Google Zanzibar. + * + * The version of the OpenAPI document: 0.1 + * Contact: community@openfga.dev + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +package dev.openfga.sdk.api.auth; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; + +/** + * A credentials flow request. It contains a Client ID and Secret that can be exchanged for an access token. + *

+ * {@see "https://auth0.com/docs/get-started/authentication-and-authorization-flow/client-credentials-flow"} + */ +@JsonPropertyOrder({ + CredentialsFlowRequest.JSON_PROPERTY_CLIENT_ID, + CredentialsFlowRequest.JSON_PROPERTY_CLIENT_SECRET, + CredentialsFlowRequest.JSON_PROPERTY_AUDIENCE, + CredentialsFlowRequest.JSON_PROPERTY_GRANT_TYPE +}) +class CredentialsFlowRequest { + public static final String JSON_PROPERTY_CLIENT_ID = "client_id"; + private String clientId; + + public static final String JSON_PROPERTY_CLIENT_SECRET = "client_secret"; + private String clientSecret; + + public static final String JSON_PROPERTY_AUDIENCE = "audience"; + private String audience; + + public static final String JSON_PROPERTY_GRANT_TYPE = "grant_type"; + private String grantType; + + @JsonCreator + public CredentialsFlowRequest() {} + + @JsonProperty(JSON_PROPERTY_CLIENT_ID) + public String getClientId() { + return clientId; + } + + @JsonProperty(JSON_PROPERTY_CLIENT_ID) + public void setClientId(String clientId) { + this.clientId = clientId; + } + + @JsonProperty(JSON_PROPERTY_CLIENT_SECRET) + public String getClientSecret() { + return clientSecret; + } + + @JsonProperty(JSON_PROPERTY_CLIENT_SECRET) + public void setClientSecret(String clientSecret) { + this.clientSecret = clientSecret; + } + + @JsonProperty(JSON_PROPERTY_AUDIENCE) + public String getAudience() { + return audience; + } + + @JsonProperty(JSON_PROPERTY_AUDIENCE) + public void setAudience(String audience) { + this.audience = audience; + } + + @JsonProperty(JSON_PROPERTY_GRANT_TYPE) + public String getGrantType() { + return grantType; + } + + @JsonProperty(JSON_PROPERTY_GRANT_TYPE) + public void setGrantType(String grantType) { + this.grantType = grantType; + } +} diff --git a/src/main/java/dev/openfga/sdk/api/auth/CredentialsFlowResponse.java b/src/main/java/dev/openfga/sdk/api/auth/CredentialsFlowResponse.java new file mode 100644 index 0000000..d9375ee --- /dev/null +++ b/src/main/java/dev/openfga/sdk/api/auth/CredentialsFlowResponse.java @@ -0,0 +1,88 @@ +/* + * OpenFGA + * A high performance and flexible authorization/permission engine built for developers and inspired by Google Zanzibar. + * + * The version of the OpenAPI document: 0.1 + * Contact: community@openfga.dev + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +package dev.openfga.sdk.api.auth; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; + +/** + * A credentials flow response. Contains an access token that can be used to authenticate + *

+ * {@see "https://auth0.com/docs/get-started/authentication-and-authorization-flow/client-credentials-flow"} + */ +@JsonPropertyOrder({ + CredentialsFlowResponse.JSON_PROPERTY_ACCESS_TOKEN, + CredentialsFlowResponse.JSON_PROPERTY_SCOPE, + CredentialsFlowResponse.JSON_PROPERTY_EXPIRES_IN, + CredentialsFlowResponse.JSON_PROPERTY_TOKEN_TYPE +}) +class CredentialsFlowResponse { + public static final String JSON_PROPERTY_ACCESS_TOKEN = "access_token"; + private String accessToken; + + public static final String JSON_PROPERTY_SCOPE = "scope"; + private String scope; + + public static final String JSON_PROPERTY_EXPIRES_IN = "expires_in"; + private long expiresInSeconds; + + public static final String JSON_PROPERTY_TOKEN_TYPE = "token_type"; + private String tokenType; + + @JsonProperty(JSON_PROPERTY_ACCESS_TOKEN) + public String getAccessToken() { + return accessToken; + } + + @JsonProperty(JSON_PROPERTY_ACCESS_TOKEN) + public void setAccessToken(String accessToken) { + this.accessToken = accessToken; + } + + @JsonProperty(JSON_PROPERTY_SCOPE) + public String getScope() { + return scope; + } + + @JsonProperty(JSON_PROPERTY_SCOPE) + public void setScope(String scope) { + this.scope = scope; + } + + /** + * The expiration time, in seconds. + *

+ * By the convention of RFC 6749 section 5.1, an expires_in value from a response will be understood + * as a value in seconds. {@see https://datatracker.ietf.org/doc/html/rfc6749#autoid-55} + * @return The expiration time, from now, in seconds + */ + @JsonProperty(JSON_PROPERTY_EXPIRES_IN) + public long getExpiresInSeconds() { + return expiresInSeconds; + } + + @JsonProperty(JSON_PROPERTY_EXPIRES_IN) + public void setExpiresInSeconds(long expiresInSeconds) { + this.expiresInSeconds = expiresInSeconds; + } + + @JsonProperty(JSON_PROPERTY_TOKEN_TYPE) + public String getTokenType() { + return tokenType; + } + + @JsonProperty(JSON_PROPERTY_TOKEN_TYPE) + public void setTokenType(String tokenType) { + this.tokenType = tokenType; + } +} diff --git a/src/main/java/dev/openfga/sdk/api/auth/OAuth2Client.java b/src/main/java/dev/openfga/sdk/api/auth/OAuth2Client.java new file mode 100644 index 0000000..5c74533 --- /dev/null +++ b/src/main/java/dev/openfga/sdk/api/auth/OAuth2Client.java @@ -0,0 +1,106 @@ +/* + * OpenFGA + * A high performance and flexible authorization/permission engine built for developers and inspired by Google Zanzibar. + * + * The version of the OpenAPI document: 0.1 + * Contact: community@openfga.dev + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +package dev.openfga.sdk.api.auth; + +import com.fasterxml.jackson.databind.ObjectMapper; +import dev.openfga.sdk.api.client.ApiClient; +import dev.openfga.sdk.api.configuration.*; +import dev.openfga.sdk.errors.ApiException; +import dev.openfga.sdk.errors.FgaInvalidParameterException; +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Instant; +import java.util.concurrent.CompletableFuture; + +public class OAuth2Client { + private final HttpClient httpClient; + private final Credentials credentials; + private final ObjectMapper mapper; + private final AccessToken token = new AccessToken(); + private final CredentialsFlowRequest authRequest; + private final String apiTokenIssuer; + + /** + * Initializes a new instance of the {@link OAuth2Client} class + * + * @param configuration Configuration, including credentials, that can be used to retrieve an access tokens + * @param httpClient Http client + */ + public OAuth2Client(Configuration configuration, HttpClient httpClient, ObjectMapper mapper) + throws FgaInvalidParameterException { + this.credentials = configuration.getCredentials(); + + this.httpClient = httpClient; + this.mapper = mapper; + this.apiTokenIssuer = credentials.getClientCredentials().getApiTokenIssuer(); + this.authRequest = new CredentialsFlowRequest(); + this.authRequest.setClientId(credentials.getClientCredentials().getClientId()); + this.authRequest.setClientSecret(credentials.getClientCredentials().getClientSecret()); + this.authRequest.setAudience(credentials.getClientCredentials().getApiAudience()); + this.authRequest.setGrantType("client_credentials"); + } + + /** + * Gets an access token, handling exchange when necessary. The access token is naively cached in memory until it + * expires. + * + * @return An access token in a {@link CompletableFuture} + */ + public CompletableFuture getAccessToken() throws FgaInvalidParameterException, ApiException { + if (!token.isValid()) { + return exchangeToken().thenCompose(response -> { + token.setToken(response.getAccessToken()); + token.setExpiresAt(Instant.now().plusSeconds(response.getExpiresInSeconds())); + return CompletableFuture.completedFuture(token.getToken()); + }); + } + + return CompletableFuture.completedFuture(token.getToken()); + } + + /** + * Exchange a client id and client secret for an access token. + * @return The credentials flow response + */ + private CompletableFuture exchangeToken() + throws ApiException, FgaInvalidParameterException { + try { + byte[] body = mapper.writeValueAsBytes(authRequest); + + Configuration config = new Configuration().apiUrl("https://" + apiTokenIssuer); + + HttpRequest request = ApiClient.requestBuilder("POST", "/oauth/token", body, config) + .build(); + + return httpClient + .sendAsync(request, HttpResponse.BodyHandlers.ofString()) + .thenCompose(httpResponse -> { + if (httpResponse.statusCode() != HttpURLConnection.HTTP_OK) { + return CompletableFuture.failedFuture(new ApiException("exchangeToken", httpResponse)); + } + try { + CredentialsFlowResponse response = + mapper.readValue(httpResponse.body(), CredentialsFlowResponse.class); + return CompletableFuture.completedFuture(response); + } catch (Exception e) { + return CompletableFuture.failedFuture(e); + } + }); + } catch (IOException e) { + throw new ApiException(e); + } + } +} diff --git a/src/main/java/dev/openfga/sdk/api/client/ApiClient.java b/src/main/java/dev/openfga/sdk/api/client/ApiClient.java index 8220120..6901fae 100644 --- a/src/main/java/dev/openfga/sdk/api/client/ApiClient.java +++ b/src/main/java/dev/openfga/sdk/api/client/ApiClient.java @@ -19,11 +19,16 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import dev.openfga.sdk.api.configuration.Configuration; +import dev.openfga.sdk.errors.FgaInvalidParameterException; +import dev.openfga.sdk.util.Pair; import java.io.InputStream; +import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; +import java.time.Duration; import java.time.OffsetDateTime; import java.time.format.DateTimeFormatter; import java.util.Collection; @@ -94,6 +99,41 @@ private static String valueToString(Object value) { return value.toString(); } + public static HttpRequest.Builder requestBuilder(String method, String path, Configuration configuration) + throws FgaInvalidParameterException { + return requestBuilder(method, path, HttpRequest.BodyPublishers.noBody(), configuration); + } + + public static HttpRequest.Builder requestBuilder( + String method, String path, byte[] body, Configuration configuration) throws FgaInvalidParameterException { + HttpRequest.Builder builder = + requestBuilder(method, path, HttpRequest.BodyPublishers.ofByteArray(body), configuration); + builder.header("content-type", "application/json"); + return builder; + } + + private static HttpRequest.Builder requestBuilder( + String method, String path, HttpRequest.BodyPublisher bodyPublisher, Configuration configuration) + throws FgaInvalidParameterException { + // verify the Configuration is valid + configuration.assertValid(); + + HttpRequest.Builder builder = HttpRequest.newBuilder(); + + builder.uri(URI.create(configuration.getApiUrl() + path)); + + builder.header("accept", "application/json"); + + builder.method(method, bodyPublisher); + + Duration readTimeout = configuration.getReadTimeout(); + if (readTimeout != null) { + builder.timeout(readTimeout); + } + + return builder; + } + /** * URL encode a string in the UTF-8 encoding. * diff --git a/src/main/java/dev/openfga/sdk/api/client/CredentialsMethod.java b/src/main/java/dev/openfga/sdk/api/client/CredentialsMethod.java deleted file mode 100644 index 5bec856..0000000 --- a/src/main/java/dev/openfga/sdk/api/client/CredentialsMethod.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * OpenFGA - * A high performance and flexible authorization/permission engine built for developers and inspired by Google Zanzibar. - * - * The version of the OpenAPI document: 0.1 - * Contact: community@openfga.dev - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ - -package dev.openfga.sdk.api.client; - -/** - * - */ -public enum CredentialsMethod { - NONE, - API_BEARER_TOKEN, - CLIENT_CREDENTIALS; - - private ApiBearerToken apiBearerToken; - private ClientCredentials clientCredentials; - - public static CredentialsMethod none() { - return NONE; - } - - public static CredentialsMethod apiBearerToken(ApiBearerToken apiBearerToken) { - CredentialsMethod it = API_BEARER_TOKEN; - it.apiBearerToken = apiBearerToken; - return it; - } - - public static CredentialsMethod clientCredentials(ClientCredentials clientCredentials) { - CredentialsMethod it = CLIENT_CREDENTIALS; - it.clientCredentials = clientCredentials; - return it; - } -} diff --git a/src/main/java/dev/openfga/sdk/api/client/RFC3339DateFormat.java b/src/main/java/dev/openfga/sdk/api/client/RFC3339DateFormat.java deleted file mode 100644 index eb342be..0000000 --- a/src/main/java/dev/openfga/sdk/api/client/RFC3339DateFormat.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * OpenFGA - * A high performance and flexible authorization/permission engine built for developers and inspired by Google Zanzibar. - * - * The version of the OpenAPI document: 0.1 - * Contact: community@openfga.dev - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ - -package dev.openfga.sdk.api.client; - -import com.fasterxml.jackson.databind.util.StdDateFormat; -import java.text.DateFormat; -import java.text.DecimalFormat; -import java.text.FieldPosition; -import java.text.ParsePosition; -import java.util.Date; -import java.util.GregorianCalendar; -import java.util.TimeZone; - -public class RFC3339DateFormat extends DateFormat { - private static final long serialVersionUID = 1L; - private static final TimeZone TIMEZONE_Z = TimeZone.getTimeZone("UTC"); - - private final StdDateFormat fmt = - new StdDateFormat().withTimeZone(TIMEZONE_Z).withColonInTimeZone(true); - - public RFC3339DateFormat() { - this.calendar = new GregorianCalendar(); - this.numberFormat = new DecimalFormat(); - } - - @Override - public Date parse(String source) { - return parse(source, new ParsePosition(0)); - } - - @Override - public Date parse(String source, ParsePosition pos) { - return fmt.parse(source, pos); - } - - @Override - public StringBuffer format(Date date, StringBuffer toAppendTo, FieldPosition fieldPosition) { - return fmt.format(date, toAppendTo, fieldPosition); - } - - @Override - public Object clone() { - return super.clone(); - } -} diff --git a/src/main/java/dev/openfga/sdk/api/client/ApiBearerToken.java b/src/main/java/dev/openfga/sdk/api/configuration/ApiToken.java similarity index 71% rename from src/main/java/dev/openfga/sdk/api/client/ApiBearerToken.java rename to src/main/java/dev/openfga/sdk/api/configuration/ApiToken.java index 03108eb..7b3c66a 100644 --- a/src/main/java/dev/openfga/sdk/api/client/ApiBearerToken.java +++ b/src/main/java/dev/openfga/sdk/api/configuration/ApiToken.java @@ -10,12 +10,16 @@ * Do not edit the class manually. */ -package dev.openfga.sdk.api.client; +package dev.openfga.sdk.api.configuration; -public class ApiBearerToken { +/** + * A static API token. In OAuth2 terms, this indicates an "access token" + * that will be used to authenticate a request. + */ +public class ApiToken { private String token; - public ApiBearerToken(String token) { + public ApiToken(String token) { this.token = token; } diff --git a/src/main/java/dev/openfga/sdk/api/client/ClientCredentials.java b/src/main/java/dev/openfga/sdk/api/configuration/ClientCredentials.java similarity index 95% rename from src/main/java/dev/openfga/sdk/api/client/ClientCredentials.java rename to src/main/java/dev/openfga/sdk/api/configuration/ClientCredentials.java index dcf859a..373ccfd 100644 --- a/src/main/java/dev/openfga/sdk/api/client/ClientCredentials.java +++ b/src/main/java/dev/openfga/sdk/api/configuration/ClientCredentials.java @@ -10,9 +10,9 @@ * Do not edit the class manually. */ -package dev.openfga.sdk.api.client; +package dev.openfga.sdk.api.configuration; -import static dev.openfga.util.StringUtil.isNullOrWhitespace; +import static dev.openfga.sdk.util.StringUtil.isNullOrWhitespace; import dev.openfga.sdk.errors.FgaInvalidParameterException; diff --git a/src/main/java/dev/openfga/sdk/api/configuration/Configuration.java b/src/main/java/dev/openfga/sdk/api/configuration/Configuration.java index 4ba4a38..030b117 100644 --- a/src/main/java/dev/openfga/sdk/api/configuration/Configuration.java +++ b/src/main/java/dev/openfga/sdk/api/configuration/Configuration.java @@ -12,7 +12,7 @@ package dev.openfga.sdk.api.configuration; -import static dev.openfga.util.StringUtil.isNullOrWhitespace; +import static dev.openfga.sdk.util.StringUtil.isNullOrWhitespace; import dev.openfga.sdk.errors.FgaInvalidParameterException; import java.net.MalformedURLException; @@ -35,6 +35,7 @@ public class Configuration implements BaseConfiguration { private static final Duration DEFAULT_CONNECT_TIMEOUT = Duration.ofSeconds(10); private String apiUrl; + private Credentials credentials; private String userAgent; private Duration readTimeout; private Duration connectTimeout; @@ -53,6 +54,14 @@ public Configuration(String apiUrl) { this.connectTimeout = DEFAULT_CONNECT_TIMEOUT; } + public Configuration(String apiUrl, Credentials credentials) { + this.apiUrl = apiUrl; + this.credentials = credentials; + this.userAgent = DEFAULT_USER_AGENT; + this.readTimeout = DEFAULT_READ_TIMEOUT; + this.connectTimeout = DEFAULT_CONNECT_TIMEOUT; + } + /** * Assert that the configuration is valid. */ @@ -77,6 +86,10 @@ public void assertValid() throws FgaInvalidParameterException { throw new FgaInvalidParameterException("hostname", "Configuration"); } } + + if (credentials != null) { + credentials.assertValid(); + } } /** @@ -91,6 +104,9 @@ public Configuration override(ConfigurationOverride configurationOverride) { String overrideApiUrl = configurationOverride.getApiUrl(); result.apiUrl(overrideApiUrl != null ? overrideApiUrl : apiUrl); + Credentials overrideCredentials = configurationOverride.getCredentials(); + result.credentials(overrideCredentials != null ? overrideCredentials : credentials); + String overrideUserAgent = configurationOverride.getUserAgent(); result.userAgent(overrideUserAgent != null ? overrideUserAgent : userAgent); @@ -149,6 +165,30 @@ public String getUserAgent() { return userAgent; } + /** + * Set the credentials. + * + * @param credentials The credentials. + * @return This object. + */ + public Configuration credentials(Credentials credentials) { + this.credentials = credentials; + return this; + } + + /** + * Get the credentials. + * + * @return The credentials. + */ + public Credentials getCredentials() { + if (this.credentials == null) { + return new Credentials(); + } + + return credentials; + } + /** * Set the read timeout for the http client. * diff --git a/src/main/java/dev/openfga/sdk/api/configuration/ConfigurationOverride.java b/src/main/java/dev/openfga/sdk/api/configuration/ConfigurationOverride.java index d317050..d2b31dc 100644 --- a/src/main/java/dev/openfga/sdk/api/configuration/ConfigurationOverride.java +++ b/src/main/java/dev/openfga/sdk/api/configuration/ConfigurationOverride.java @@ -25,12 +25,14 @@ */ public class ConfigurationOverride implements BaseConfiguration { private String apiUrl; + private Credentials credentials; private String userAgent; private Duration readTimeout; private Duration connectTimeout; public ConfigurationOverride() { this.apiUrl = null; + this.credentials = null; this.userAgent = null; this.readTimeout = null; this.connectTimeout = null; @@ -57,6 +59,26 @@ public String getApiUrl() { return apiUrl; } + /** + * Set the credentials. + * + * @param credentials The credentials. + * @return This object. + */ + public ConfigurationOverride credentials(Credentials credentials) { + this.credentials = credentials; + return this; + } + + /** + * Get the configured credentials. + * + * @return The credentials. + */ + public Credentials getCredentials() { + return credentials; + } + /** * Set the user agent. * diff --git a/src/main/java/dev/openfga/sdk/api/configuration/Credentials.java b/src/main/java/dev/openfga/sdk/api/configuration/Credentials.java new file mode 100644 index 0000000..f9ddde2 --- /dev/null +++ b/src/main/java/dev/openfga/sdk/api/configuration/Credentials.java @@ -0,0 +1,69 @@ +/* + * OpenFGA + * A high performance and flexible authorization/permission engine built for developers and inspired by Google Zanzibar. + * + * The version of the OpenAPI document: 0.1 + * Contact: community@openfga.dev + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +package dev.openfga.sdk.api.configuration; + +import dev.openfga.sdk.errors.FgaInvalidParameterException; + +public class Credentials { + private CredentialsMethod credentialsMethod; + private ApiToken apiToken; + private ClientCredentials clientCredentials; + + public Credentials() { + this.credentialsMethod = CredentialsMethod.NONE; + } + + public Credentials(ApiToken apiToken) { + this.credentialsMethod = CredentialsMethod.API_TOKEN; + this.apiToken = apiToken; + } + + public Credentials(ClientCredentials clientCredentials) { + this.credentialsMethod = CredentialsMethod.CLIENT_CREDENTIALS; + this.clientCredentials = clientCredentials; + } + + public void assertValid() throws FgaInvalidParameterException { + if (credentialsMethod == CredentialsMethod.API_TOKEN && apiToken == null) { + throw new FgaInvalidParameterException("apiToken", "Credentials"); + } + + if (credentialsMethod == CredentialsMethod.CLIENT_CREDENTIALS && clientCredentials == null) { + throw new FgaInvalidParameterException("clientCredentials", "Credentials"); + } + } + + public void setCredentialsMethod(CredentialsMethod credentialsMethod) { + this.credentialsMethod = credentialsMethod; + } + + public CredentialsMethod getCredentialsMethod() { + return credentialsMethod; + } + + public void setApiToken(ApiToken apiToken) { + this.apiToken = apiToken; + } + + public ApiToken getApiToken() { + return apiToken; + } + + public void setClientCredentials(ClientCredentials clientCredentials) { + this.clientCredentials = clientCredentials; + } + + public ClientCredentials getClientCredentials() { + return clientCredentials; + } +} diff --git a/src/main/java/dev/openfga/sdk/api/configuration/CredentialsMethod.java b/src/main/java/dev/openfga/sdk/api/configuration/CredentialsMethod.java new file mode 100644 index 0000000..5001ad3 --- /dev/null +++ b/src/main/java/dev/openfga/sdk/api/configuration/CredentialsMethod.java @@ -0,0 +1,37 @@ +/* + * OpenFGA + * A high performance and flexible authorization/permission engine built for developers and inspired by Google Zanzibar. + * + * The version of the OpenAPI document: 0.1 + * Contact: community@openfga.dev + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +package dev.openfga.sdk.api.configuration; + +/** + * Mutually exclusive methods for delivering credentials. + */ +public enum CredentialsMethod { + /** + * No credentials. + */ + NONE, + + /** + * A static API token. In OAuth2 terms, this indicates an "access token" + * that will be used to make a request. When used as part of {@link Configuration} + * then an {@link ApiToken} should also be defined. + */ + API_TOKEN, + + /** + * OAuth2 client credentials that can be used to acquire an OAuth2 access + * token. When used as part of {@link Configuration} then a + * {@link ClientCredentials} should also be defined. + */ + CLIENT_CREDENTIALS; +} diff --git a/src/main/java/dev/openfga/sdk/api/client/ApiException.java b/src/main/java/dev/openfga/sdk/errors/ApiException.java similarity index 79% rename from src/main/java/dev/openfga/sdk/api/client/ApiException.java rename to src/main/java/dev/openfga/sdk/errors/ApiException.java index 7b567e9..07522fc 100644 --- a/src/main/java/dev/openfga/sdk/api/client/ApiException.java +++ b/src/main/java/dev/openfga/sdk/errors/ApiException.java @@ -10,9 +10,10 @@ * Do not edit the class manually. */ -package dev.openfga.sdk.api.client; +package dev.openfga.sdk.errors; import java.net.http.HttpHeaders; +import java.net.http.HttpResponse; public class ApiException extends Exception { private int code = 0; @@ -60,6 +61,14 @@ public ApiException(int code, String message, HttpHeaders responseHeaders, Strin this.responseBody = responseBody; } + public ApiException(String operationId, HttpResponse response) { + this( + response.statusCode(), + formatExceptionMessage(operationId, response.statusCode(), response.body()), + response.headers(), + response.body()); + } + /** * Get the HTTP status code. * @@ -86,4 +95,11 @@ public HttpHeaders getResponseHeaders() { public String getResponseBody() { return responseBody; } + + private static String formatExceptionMessage(String operationId, int statusCode, String body) { + if (body == null || body.isEmpty()) { + body = "[no body]"; + } + return operationId + " call failed with: " + statusCode + " - " + body; + } } diff --git a/src/main/java/dev/openfga/sdk/api/client/Pair.java b/src/main/java/dev/openfga/sdk/util/Pair.java similarity index 96% rename from src/main/java/dev/openfga/sdk/api/client/Pair.java rename to src/main/java/dev/openfga/sdk/util/Pair.java index 7068739..772ee9c 100644 --- a/src/main/java/dev/openfga/sdk/api/client/Pair.java +++ b/src/main/java/dev/openfga/sdk/util/Pair.java @@ -10,7 +10,7 @@ * Do not edit the class manually. */ -package dev.openfga.sdk.api.client; +package dev.openfga.sdk.util; public class Pair { private String name = ""; diff --git a/src/main/java/dev/openfga/util/StringUtil.java b/src/main/java/dev/openfga/sdk/util/StringUtil.java similarity index 97% rename from src/main/java/dev/openfga/util/StringUtil.java rename to src/main/java/dev/openfga/sdk/util/StringUtil.java index 4528525..b9e2267 100644 --- a/src/main/java/dev/openfga/util/StringUtil.java +++ b/src/main/java/dev/openfga/sdk/util/StringUtil.java @@ -10,7 +10,7 @@ * Do not edit the class manually. */ -package dev.openfga.util; +package dev.openfga.sdk.util; import java.util.function.Predicate; import java.util.regex.Pattern; diff --git a/src/test-integration/java/dev/openfga/sdk/api/OpenFgaApiIntegrationTest.java b/src/test-integration/java/dev/openfga/sdk/api/OpenFgaApiIntegrationTest.java index f2ddc76..3cc1196 100644 --- a/src/test-integration/java/dev/openfga/sdk/api/OpenFgaApiIntegrationTest.java +++ b/src/test-integration/java/dev/openfga/sdk/api/OpenFgaApiIntegrationTest.java @@ -37,7 +37,7 @@ public class OpenFgaApiIntegrationTest { private OpenFgaApi api; @BeforeEach - public void initializeApi() { + public void initializeApi() throws Exception { Configuration apiConfig = new Configuration("http://localhost:8080"); ApiClient apiClient = new ApiClient(HttpClient.newBuilder(), mapper); api = new OpenFgaApi(apiClient, apiConfig); @@ -285,7 +285,7 @@ public void write_readAssertions() throws Exception { .assertions(List.of(new Assertion().tupleKey(DEFAULT_TUPLE_KEY).expectation(true))); // When - api.writeAssertions(storeId, authModelId, writeRequest); + api.writeAssertions(storeId, authModelId, writeRequest).get(); ReadAssertionsResponse response = api.readAssertions(storeId, authModelId).get(); diff --git a/src/test/java/dev/openfga/sdk/api/OpenFgaApiTest.java b/src/test/java/dev/openfga/sdk/api/OpenFgaApiTest.java index 0f0c243..16365e5 100644 --- a/src/test/java/dev/openfga/sdk/api/OpenFgaApiTest.java +++ b/src/test/java/dev/openfga/sdk/api/OpenFgaApiTest.java @@ -49,12 +49,13 @@ public class OpenFgaApiTest { private HttpClientMock mockHttpClient; @BeforeEach - public void beforeEachTest() { + public void beforeEachTest() throws Exception { mockHttpClient = new HttpClientMock(); mockConfiguration = mock(Configuration.class); when(mockConfiguration.getApiUrl()).thenReturn("https://localhost"); when(mockConfiguration.getReadTimeout()).thenReturn(Duration.ofMillis(250)); + when(mockConfiguration.getCredentials()).thenReturn(new Credentials()); mockApiClient = mock(ApiClient.class); when(mockApiClient.getObjectMapper()).thenReturn(mapper); diff --git a/src/test/java/dev/openfga/sdk/api/auth/OAuth2ClientTest.java b/src/test/java/dev/openfga/sdk/api/auth/OAuth2ClientTest.java new file mode 100644 index 0000000..a311150 --- /dev/null +++ b/src/test/java/dev/openfga/sdk/api/auth/OAuth2ClientTest.java @@ -0,0 +1,57 @@ +package dev.openfga.sdk.api.auth; + +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.pgssoft.httpclient.HttpClientMock; +import dev.openfga.sdk.api.configuration.*; +import dev.openfga.sdk.errors.FgaInvalidParameterException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class OAuth2ClientTest { + private static final String CLIENT_ID = "client"; + private static final String CLIENT_SECRET = "secret"; + private static final String AUDIENCE = "audience"; + private static final String GRANT_TYPE = "client_credentials"; + private static final String API_TOKEN_ISSUER = "test.fga.dev"; + private static final String POST_URL = "https://" + API_TOKEN_ISSUER + "/oauth/token"; + private static final String ACCESS_TOKEN = "0123456789"; + + private final ObjectMapper mapper = new ObjectMapper(); + private HttpClientMock mockHttpClient; + + private Credentials credentials; + private OAuth2Client oAuth2; + + @BeforeEach + public void setup() throws FgaInvalidParameterException { + mockHttpClient = new HttpClientMock(); + + credentials = new Credentials(new ClientCredentials() + .clientId(CLIENT_ID) + .clientSecret(CLIENT_SECRET) + .apiAudience(AUDIENCE) + .apiTokenIssuer(API_TOKEN_ISSUER)); + + oAuth2 = new OAuth2Client(new Configuration("", credentials), mockHttpClient, mapper); + } + + @Test + public void exchangeToken() throws Exception { + // Given + String expectedPostBody = String.format( + "{\"client_id\":\"%s\",\"client_secret\":\"%s\",\"audience\":\"%s\",\"grant_type\":\"%s\"}", + CLIENT_ID, CLIENT_SECRET, AUDIENCE, GRANT_TYPE); + String responseBody = String.format("{\"access_token\":\"%s\"}", ACCESS_TOKEN); + mockHttpClient.onPost(POST_URL).withBody(is(expectedPostBody)).doReturn(200, responseBody); + + // When + String result = oAuth2.getAccessToken().get(); + + // Then + mockHttpClient.verify().post(POST_URL).withBody(is(expectedPostBody)).called(); + assertEquals(ACCESS_TOKEN, result); + } +} diff --git a/src/test/java/dev/openfga/sdk/api/client/ClientCredentialsTest.java b/src/test/java/dev/openfga/sdk/api/configuration/ClientCredentialsTest.java similarity index 99% rename from src/test/java/dev/openfga/sdk/api/client/ClientCredentialsTest.java rename to src/test/java/dev/openfga/sdk/api/configuration/ClientCredentialsTest.java index 6ae5606..3b09e70 100644 --- a/src/test/java/dev/openfga/sdk/api/client/ClientCredentialsTest.java +++ b/src/test/java/dev/openfga/sdk/api/configuration/ClientCredentialsTest.java @@ -10,7 +10,7 @@ * Do not edit the class manually. */ -package dev.openfga.sdk.api.client; +package dev.openfga.sdk.api.configuration; import static org.junit.jupiter.api.Assertions.*; diff --git a/src/test/java/dev/openfga/util/StringUtilTest.java b/src/test/java/dev/openfga/sdk/util/StringUtilTest.java similarity index 92% rename from src/test/java/dev/openfga/util/StringUtilTest.java rename to src/test/java/dev/openfga/sdk/util/StringUtilTest.java index 0a92d00..3a4d4bb 100644 --- a/src/test/java/dev/openfga/util/StringUtilTest.java +++ b/src/test/java/dev/openfga/sdk/util/StringUtilTest.java @@ -10,9 +10,9 @@ * Do not edit the class manually. */ -package dev.openfga.util; +package dev.openfga.sdk.util; -import static dev.openfga.util.StringUtil.isNullOrWhitespace; +import static dev.openfga.sdk.util.StringUtil.isNullOrWhitespace; import static org.junit.jupiter.api.Assertions.*; import org.junit.jupiter.api.Test;