Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 32 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ This is an autogenerated Java SDK for OpenFGA. It provides a wrapper around the
- [Assertions](#assertions)
- [Read Assertions](#read-assertions)
- [Write Assertions](#write-assertions)
- [Retries](#retries)
- [API Endpoints](#api-endpoints)
- [Models](#models)
- [Contributing](#contributing)
Expand Down Expand Up @@ -133,7 +134,7 @@ import java.net.http.HttpClient;
public class Example {
public static void main(String[] args) throws Exception {
var config = new ClientConfiguration()
.apiUrl(System.getenv("FGA_API_URL")) // If not specified, will default to "https://localhost:8080"
.apiUrl(System.getenv("FGA_API_URL")) // If not specified, will default to "http://localhost:8080"
.storeId(System.getenv("FGA_STORE_ID")) // Not required when calling createStore() or listStores()
.authorizationModelId(System.getenv("FGA_AUTHORIZATION_MODEL_ID")); // Optional, can be overridden per request

Expand All @@ -156,7 +157,7 @@ import java.net.http.HttpClient;
public class Example {
public static void main(String[] args) throws Exception {
var config = new ClientConfiguration()
.apiUrl(System.getenv("FGA_API_URL")) // If not specified, will default to "https://localhost:8080"
.apiUrl(System.getenv("FGA_API_URL")) // If not specified, will default to "http://localhost:8080"
.storeId(System.getenv("FGA_STORE_ID")) // Not required when calling createStore() or listStores()
.authorizationModelId(System.getenv("FGA_AUTHORIZATION_MODEL_ID")) // Optional, can be overridden per request
.credentials(new Credentials(
Expand All @@ -182,7 +183,7 @@ import java.net.http.HttpClient;
public class Example {
public static void main(String[] args) throws Exception {
var config = new ClientConfiguration()
.apiUrl(System.getenv("FGA_API_URL")) // If not specified, will default to "https://localhost:8080"
.apiUrl(System.getenv("FGA_API_URL")) // If not specified, will default to "http://localhost:8080"
.storeId(System.getenv("FGA_STORE_ID")) // Not required when calling createStore() or listStores()
.authorizationModelId(System.getenv("FGA_AUTHORIZATION_MODEL_ID")) // Optional, can be overridden per request
.credentials(new Credentials(
Expand Down Expand Up @@ -212,7 +213,7 @@ import java.net.http.HttpClient;
public class Example {
public static void main(String[] args) throws Exception {
var config = new ClientConfiguration()
.apiUrl(System.getenv("FGA_API_URL")) // If not specified, will default to "https://localhost:8080"
.apiUrl(System.getenv("FGA_API_URL")) // If not specified, will default to "http://localhost:8080"
.storeId(System.getenv("FGA_STORE_ID")) // Not required when calling createStore() or listStores()
.authorizationModelId(System.getenv("FGA_AUTHORIZATION_MODEL_ID")) // Optional, can be overridden per request
.credentials(new Credentials(
Expand Down Expand Up @@ -805,6 +806,33 @@ fgaClient.writeAssertions(assertions, options).get();
```


### Retries

If a network request fails with a 429 or 5xx error from the server, the SDK will automatically retry the request up to 15 times with a minimum wait time of 100 milliseconds between each attempt.

To customize this behavior, call `maxRetries` and `minimumRetryDelay` on the `ClientConfiguration` builder. `maxRetries` determines the maximum number of retries (up to 15), while `minimumRetryDelay` sets the minimum wait time between retries in milliseconds.

```java
import com.fasterxml.jackson.databind.ObjectMapper;
import dev.openfga.sdk.api.client.OpenFgaClient;
import dev.openfga.sdk.api.configuration.ClientConfiguration;
import java.net.http.HttpClient;

public class Example {
public static void main(String[] args) throws Exception {
var config = new ClientConfiguration()
.apiUrl(System.getenv("FGA_API_URL")) // If not specified, will default to "http://localhost:8080"
.storeId(System.getenv("FGA_STORE_ID")) // Not required when calling createStore() or listStores()
.authorizationModelId(System.getenv("FGA_AUTHORIZATION_MODEL_ID")) // Optional, can be overridden per request
.maxRetries(3) // retry up to 3 times on API requests
.minimumRetryDelay(250); // wait a minimum of 250 milliseconds between requests

var fgaClient = new OpenFgaClient(config);
var response = fgaClient.readAuthorizationModels().get();
}
}
```

### API Endpoints

| Method | HTTP request | Description |
Expand Down
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ testing {
implementation "org.junit.jupiter:junit-jupiter:$junit_version"
implementation "org.mockito:mockito-core:5.+"
runtimeOnly "org.junit.platform:junit-platform-launcher"
implementation "org.wiremock:wiremock:3.5.2"
Comment thread
jimmyjames marked this conversation as resolved.

// This test-only dependency is convenient but not widely used.
// Review project activity before updating the version here.
Expand Down
9 changes: 5 additions & 4 deletions src/main/java/dev/openfga/sdk/api/auth/OAuth2Client.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ public class OAuth2Client {
private static final String DEFAULT_API_TOKEN_ISSUER_PATH = "/oauth/token";

private final ApiClient apiClient;
private final String apiTokenIssuer;
private final AccessToken token = new AccessToken();
private final CredentialsFlowRequest authRequest;
private final Configuration config;

/**
* Initializes a new instance of the {@link OAuth2Client} class
Expand All @@ -38,11 +38,14 @@ public OAuth2Client(Configuration configuration, ApiClient apiClient) throws Fga
var clientCredentials = configuration.getCredentials().getClientCredentials();

this.apiClient = apiClient;
this.apiTokenIssuer = buildApiTokenIssuer(clientCredentials.getApiTokenIssuer());
this.authRequest =
new CredentialsFlowRequest(clientCredentials.getClientId(), clientCredentials.getClientSecret());
this.authRequest.setAudience(clientCredentials.getApiAudience());
this.authRequest.setScope(clientCredentials.getScopes());
this.config = new Configuration()
.apiUrl(buildApiTokenIssuer(clientCredentials.getApiTokenIssuer()))
.maxRetries(configuration.getMaxRetries())
.minimumRetryDelay(configuration.getMinimumRetryDelay());
}

/**
Expand Down Expand Up @@ -70,8 +73,6 @@ public CompletableFuture<String> getAccessToken() throws FgaInvalidParameterExce
private CompletableFuture<CredentialsFlowResponse> exchangeToken()
throws ApiException, FgaInvalidParameterException {

Configuration config = new Configuration().apiUrl(apiTokenIssuer);

HttpRequest.Builder requestBuilder =
ApiClient.formRequestBuilder("POST", "", this.authRequest.buildFormRequestBody(), config);
HttpRequest request = requestBuilder.build();
Expand Down
91 changes: 82 additions & 9 deletions src/test/java/dev/openfga/sdk/api/auth/OAuth2ClientTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,25 +12,30 @@

package dev.openfga.sdk.api.auth;

import static com.github.tomakehurst.wiremock.client.WireMock.*;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.core.StringContains.containsString;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.tomakehurst.wiremock.junit5.*;
import com.github.tomakehurst.wiremock.stubbing.Scenario;
import com.pgssoft.httpclient.HttpClientMock;
import dev.openfga.sdk.api.client.ApiClient;
import dev.openfga.sdk.api.configuration.*;
import dev.openfga.sdk.errors.FgaInvalidParameterException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

@WireMockTest
class OAuth2ClientTest {
private static final String CLIENT_ID = "client";
private static final String CLIENT_SECRET = "secret";
Expand Down Expand Up @@ -126,6 +131,49 @@ public void exchangeOAuth2Token(String apiTokenIssuer, String tokenEndpointUrl)
assertEquals(ACCESS_TOKEN, result);
}

@Test
public void exchangeOAuth2TokenWithRetriesSuccess(WireMockRuntimeInfo wm) throws Exception {
// Return 429 initially
stubFor(post(urlEqualTo("/oauth/token"))
.inScenario("retries")
.whenScenarioStateIs(Scenario.STARTED)
.willReturn(jsonResponse("rate_limited", 429))
.willSetStateTo("rate limited once"));

// Then return 500
stubFor(post(urlEqualTo("/oauth/token"))
.inScenario("retries")
.whenScenarioStateIs("rate limited once")
.willReturn(jsonResponse("rate_limited", 500))
.willSetStateTo("rate limited twice"));

// Finally return 200
stubFor(post(urlEqualTo("/oauth/token"))
.inScenario("retries")
.whenScenarioStateIs("rate limited twice")
.willReturn(ok(String.format("{\"access_token\":\"%s\"}", ACCESS_TOKEN))));

OAuth2Client auth0 = newOAuth2Client(wm.getHttpBaseUrl(), false);

String result = auth0.getAccessToken().get();

assertEquals(ACCESS_TOKEN, result);
verify(3, postRequestedFor(urlEqualTo("/oauth/token")));
}

@Test
public void exchangeOAuth2TokenWithRetriesFailure(WireMockRuntimeInfo wm) throws Exception {
stubFor(post(urlEqualTo("/oauth/token")).willReturn(jsonResponse("error", 429)));

OAuth2Client auth0 = newOAuth2Client(wm.getHttpBaseUrl(), false);

var exception = assertThrows(java.util.concurrent.ExecutionException.class, () -> auth0.getAccessToken()
.get());

assertEquals("dev.openfga.sdk.errors.FgaApiRateLimitExceededError: exchangeToken", exception.getMessage());
verify(3, postRequestedFor(urlEqualTo("/oauth/token")));
}

@Test
public void apiTokenIssuer_invalidScheme() {
// When
Expand Down Expand Up @@ -164,7 +212,8 @@ private OAuth2Client newAuth0Client(String apiTokenIssuer) throws FgaInvalidPara
.clientId(CLIENT_ID)
.clientSecret(CLIENT_SECRET)
.apiAudience(AUDIENCE)
.apiTokenIssuer(apiTokenIssuer)));
.apiTokenIssuer(apiTokenIssuer)),
true);
}

private OAuth2Client newOAuth2Client(String apiTokenIssuer) throws FgaInvalidParameterException {
Expand All @@ -174,21 +223,45 @@ private OAuth2Client newOAuth2Client(String apiTokenIssuer) throws FgaInvalidPar
.clientId(CLIENT_ID)
.clientSecret(CLIENT_SECRET)
.scopes(SCOPES)
.apiTokenIssuer(apiTokenIssuer)));
.apiTokenIssuer(apiTokenIssuer)),
true);
}

private OAuth2Client newOAuth2Client(String apiTokenIssuer, Boolean useMockHttpClient)
throws FgaInvalidParameterException {
return newClientCredentialsClient(
apiTokenIssuer,
new Credentials(new ClientCredentials()
.clientId(CLIENT_ID)
.clientSecret(CLIENT_SECRET)
.scopes(SCOPES)
.apiTokenIssuer(apiTokenIssuer)),
useMockHttpClient);
}

private OAuth2Client newClientCredentialsClient(String apiTokenIssuer, Credentials credentials)
private OAuth2Client newClientCredentialsClient(
String apiTokenIssuer, Credentials credentials, Boolean useMockHttpClient)
throws FgaInvalidParameterException {
System.setProperty("HttpRequestAttempt.debug-logging", "enable");

mockHttpClient = new HttpClientMock();
mockHttpClient.debugOn();
var configuration = new Configuration()
.apiUrl("")
.credentials(credentials)
.maxRetries(2)
.minimumRetryDelay(Duration.ofMillis(10));

var configuration = new Configuration().apiUrl("").credentials(credentials);
// If requested, enable the HttpClientMock and set that as the HttpClient to use in ApiClient
ApiClient apiClient;
if (useMockHttpClient) {
mockHttpClient = new HttpClientMock();
mockHttpClient.debugOn();

var apiClient = mock(ApiClient.class);
when(apiClient.getHttpClient()).thenReturn(mockHttpClient);
when(apiClient.getObjectMapper()).thenReturn(mapper);
apiClient = mock(ApiClient.class);
when(apiClient.getHttpClient()).thenReturn(mockHttpClient);
when(apiClient.getObjectMapper()).thenReturn(mapper);
} else {
apiClient = new ApiClient();
}

return new OAuth2Client(configuration, apiClient);
}
Expand Down