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
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1596,6 +1596,22 @@ try {

```

#### Routing FGA calls to an FGA cache

If you run an FGA cache (authzcache) instance, set `fgaCacheUrl` on the config, or the
`DESCOPE_FGA_CACHE_URL` env var, and the calls it serves are sent there instead of the Descope base URL:
`saveSchema`, `createRelations`, `deleteRelations` and `check` on the FGA service, plus `whoCanAccess` and
`whatCanTargetAccess` on the authz service. Every other call, including `loadSchema`, `dryRunSchema` and the
resource details calls, stays on the base URL. Leave it unset to send everything to the base URL.

```java
var descopeClient = new DescopeClient(Config.builder()
.projectId("Your-project")
.managementKey("management-key")
.fgaCacheUrl("https://your-authzcache-host")
.build());
```

### Manage Outbound Applications

You can fetch, delete, and manage outbound application tokens:
Expand Down
16 changes: 16 additions & 0 deletions src/main/java/com/descope/client/Config.java
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,15 @@ public class Config {
// with descope services.
private Map<String, String> customDefaultHeaders;

// FGACacheURL (optional, "") - pass FGA calls through a cache service, if set.
private String fgaCacheUrl;

// Keeps the pre-fgaCacheUrl all-args constructor available to callers that use it positionally.
public Config(String projectId, String managementKey, String publicKey, String descopeBaseUrl,
Map<String, String> customDefaultHeaders) {
this(projectId, managementKey, publicKey, descopeBaseUrl, customDefaultHeaders, null);
}

public String initializeProjectId() {
if (StringUtils.isBlank(this.projectId)) {
this.projectId = EnvironmentUtils.getProjectId();
Expand All @@ -60,6 +69,13 @@ public String initializeBaseURL() {
return this.descopeBaseUrl;
}

public String initializeFgaCacheUrl() {
if (StringUtils.isBlank(this.fgaCacheUrl)) {
this.fgaCacheUrl = EnvironmentUtils.getFgaCacheURL();
}
return this.fgaCacheUrl;
}

public String initializePublicKey() {
if (StringUtils.isBlank(this.publicKey)) {
this.publicKey = EnvironmentUtils.getPublicKey();
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/com/descope/client/DescopeClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ public DescopeClient(Config config) throws DescopeException {
}
config.initializeManagementKey();
config.initializeBaseURL();
config.initializeFgaCacheUrl();

Client client = getClient(config);
this.authenticationServices = AuthenticationServiceBuilder.buildServices(client);
Expand All @@ -67,6 +68,7 @@ private static Client getClient(Config config) {
final String baseUrl = DEFAULT_BASE_URL.replace(REGION_PLACEHOLDER, region.length() > 3 ? region + "." : "");
Client c = Client.builder()
.uri(StringUtils.isBlank(config.getDescopeBaseUrl()) ? baseUrl : config.getDescopeBaseUrl())
.fgaCacheUri(config.getFgaCacheUrl())
.projectId(projectId)
.managementKey(config.getManagementKey())
.headers(
Expand Down
1 change: 1 addition & 0 deletions src/main/java/com/descope/literals/AppConstants.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ public class AppConstants {
public static final String PUBLIC_KEY_ENV_VAR = "DESCOPE_PUBLIC_KEY";
public static final String MANAGEMENT_KEY_ENV_VAR = "DESCOPE_MANAGEMENT_KEY";
public static final String BASE_URL_ENV_VAR = "DESCOPE_BASE_URL";
public static final String FGA_CACHE_URL_ENV_VAR = "DESCOPE_FGA_CACHE_URL";
public static final String AUTHORIZATION_HEADER_NAME = "Authorization";
public static final String BEARER_AUTHORIZATION_PREFIX = "Bearer ";
public static final String COOKIE = "Cookie";
Expand Down
8 changes: 8 additions & 0 deletions src/main/java/com/descope/model/client/Client.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ public class Client {
private Key providedKey;
@Builder.Default
private AtomicReference<Map<String, Key>> keys = new AtomicReference<>(new HashMap<>());
// When set, FGA calls that the FGA cache serves go here instead of uri.
private String fgaCacheUri;

// Keeps the pre-fgaCacheUri all-args constructor available to callers that use it positionally.
public Client(String uri, String projectId, String managementKey, Map<String, String> headers,
SdkInfo sdkInfo, Key providedKey, AtomicReference<Map<String, Key>> keys) {
this(uri, projectId, managementKey, headers, sdkInfo, providedKey, keys, null);
}

public Key getKey(String keyId) {
if (providedKey != null) {
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/com/descope/sdk/mgmt/impl/AuthzServiceImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ public List<String> whoCanAccess(String resource, String relationDefinition, Str
if (context != null && !context.isEmpty()) {
request.put("context", context);
}
WhoCanAccessResponse resp = apiProxy.post(getUri(MANAGEMENT_AUTHZ_RE_WHO), request, WhoCanAccessResponse.class);
WhoCanAccessResponse resp = apiProxy.post(getFgaUri(MANAGEMENT_AUTHZ_RE_WHO), request, WhoCanAccessResponse.class);
return resp.getTargets();
}

Expand Down Expand Up @@ -252,7 +252,7 @@ public List<Relation> whatCanTargetAccess(String target, Map<String, Object> con
if (context != null && !context.isEmpty()) {
request.put("context", context);
}
RelationsResponse resp = apiProxy.post(getUri(MANAGEMENT_AUTHZ_RE_TARGET_ALL), request, RelationsResponse.class);
RelationsResponse resp = apiProxy.post(getFgaUri(MANAGEMENT_AUTHZ_RE_TARGET_ALL), request, RelationsResponse.class);
return resp.getRelations();
}

Expand Down
8 changes: 4 additions & 4 deletions src/main/java/com/descope/sdk/mgmt/impl/FGAServiceImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public void saveSchema(FGASchema schema) throws DescopeException {
requestBody.put("dsl", schema.getDsl());

ApiProxy apiProxy = getApiProxy();
apiProxy.post(getUri(MANAGEMENT_FGA_SAVE_SCHEMA), requestBody, Void.class);
apiProxy.post(getFgaUri(MANAGEMENT_FGA_SAVE_SCHEMA), requestBody, Void.class);
}

@Override
Expand Down Expand Up @@ -91,7 +91,7 @@ public void createRelations(List<FGARelation> relations) throws DescopeException
requestBody.put("tuples", relations);

ApiProxy apiProxy = getApiProxy();
apiProxy.post(getUri(MANAGEMENT_FGA_CREATE_RELATIONS), requestBody, Void.class);
apiProxy.post(getFgaUri(MANAGEMENT_FGA_CREATE_RELATIONS), requestBody, Void.class);
}

@Override
Expand All @@ -104,7 +104,7 @@ public void deleteRelations(List<FGARelation> relations) throws DescopeException
requestBody.put("tuples", relations);

ApiProxy apiProxy = getApiProxy();
apiProxy.post(getUri(MANAGEMENT_FGA_DELETE_RELATIONS), requestBody, Void.class);
apiProxy.post(getFgaUri(MANAGEMENT_FGA_DELETE_RELATIONS), requestBody, Void.class);
}

@Override
Expand All @@ -126,7 +126,7 @@ public List<FGACheckResult> check(List<FGARelation> relations, Map<String, Objec
}

ApiProxy apiProxy = getApiProxy();
FGACheckResponse response = apiProxy.post(getUri(MANAGEMENT_FGA_CHECK), requestBody, FGACheckResponse.class);
FGACheckResponse response = apiProxy.post(getFgaUri(MANAGEMENT_FGA_CHECK), requestBody, FGACheckResponse.class);

List<FGACheckResult> results = new ArrayList<>();
if (response == null || response.getTuples() == null) {
Expand Down
12 changes: 12 additions & 0 deletions src/main/java/com/descope/sdk/mgmt/impl/ManagementsBase.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import com.descope.proxy.impl.ApiProxyBuilder;
import com.descope.sdk.SdkServicesBase;
import com.descope.sdk.mgmt.ManagementService;
import com.descope.utils.UriUtils;
import java.net.URI;
import org.apache.commons.lang3.StringUtils;

abstract class ManagementsBase extends SdkServicesBase implements ManagementService {
Expand Down Expand Up @@ -41,4 +43,14 @@ ApiProxy getApiProxyWithBearer(String bearerJwt) {
String token = String.format("Bearer %s", bearerJwt);
return ApiProxyBuilder.buildProxy(() -> token, client);
}

// FGA calls that the FGA cache serves go to it when one is configured, everything else
// stays on the Descope base URL.
URI getFgaUri(String path) {
String fgaCacheUri = client.getFgaCacheUri();
if (StringUtils.isBlank(fgaCacheUri)) {
return getUri(path);
}
return UriUtils.getUri(StringUtils.removeEnd(fgaCacheUri, "/"), path);
}
}
5 changes: 5 additions & 0 deletions src/main/java/com/descope/utils/EnvironmentUtils.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.descope.utils;

import static com.descope.literals.AppConstants.BASE_URL_ENV_VAR;
import static com.descope.literals.AppConstants.FGA_CACHE_URL_ENV_VAR;
import static com.descope.literals.AppConstants.MANAGEMENT_KEY_ENV_VAR;
import static com.descope.literals.AppConstants.PROJECT_ID_ENV_VAR;
import static com.descope.literals.AppConstants.PUBLIC_KEY_ENV_VAR;
Expand All @@ -20,6 +21,10 @@ public static String getBaseURL() {
return dotenv.get(BASE_URL_ENV_VAR);
}

public static String getFgaCacheURL() {
return dotenv.get(FGA_CACHE_URL_ENV_VAR);
}

public static String getPublicKey() {
return dotenv.get(PUBLIC_KEY_ENV_VAR);
}
Expand Down
24 changes: 24 additions & 0 deletions src/test/java/com/descope/sdk/mgmt/impl/AuthzServiceImplTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

import com.descope.exception.RateLimitExceededException;
Expand All @@ -34,6 +35,7 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import java.io.File;
import java.net.URI;
import java.time.Instant;
import java.time.Period;
import java.util.Arrays;
Expand Down Expand Up @@ -441,6 +443,28 @@ void testWhatCanTargetAccessForSuccess() {
}
}

@Test
void testFgaCacheRouting() {
Client client = Client.builder().uri("https://api.descope.com").fgaCacheUri("https://cache.example.com/")
.projectId("someProjectId").managementKey("someManagementKey").build();
AuthzService cachedAuthzService = ManagementServiceBuilder.buildServices(client).getAuthzService();
ApiProxy apiProxy = mock(ApiProxy.class);
doReturn(new RelationsResponse(Arrays.asList(new Relation()))).when(apiProxy).post(any(), any(), any());
try (MockedStatic<ApiProxyBuilder> mockedApiProxyBuilder = mockStatic(ApiProxyBuilder.class)) {
mockedApiProxyBuilder.when(
() -> ApiProxyBuilder.buildProxy(any(), any())).thenReturn(apiProxy);
cachedAuthzService.whatCanTargetAccess("kiki");
cachedAuthzService.resourceRelations("kuku");

ArgumentCaptor<URI> uriCaptor = ArgumentCaptor.forClass(URI.class);
verify(apiProxy, times(2)).post(uriCaptor.capture(), any(), any());
assertEquals("https://cache.example.com/v1/mgmt/authz/re/targetall",
uriCaptor.getAllValues().get(0).toString());
assertEquals("https://api.descope.com/v1/mgmt/authz/re/resource",
uriCaptor.getAllValues().get(1).toString());
}
}

@SuppressWarnings("unchecked")
@Test
void testWhatCanTargetAccessWithContext() {
Expand Down
36 changes: 36 additions & 0 deletions src/test/java/com/descope/sdk/mgmt/impl/FGAServiceImplTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

Expand Down Expand Up @@ -35,6 +36,7 @@
import com.descope.sdk.mgmt.FGAService;
import com.fasterxml.jackson.core.type.TypeReference;
import java.io.IOException;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Arrays;
Expand Down Expand Up @@ -319,6 +321,40 @@ void testLoadResourcesDetails_Success() throws Exception {
}
}

@Test
void testFgaCacheRouting() throws Exception {
when(client.getUri()).thenReturn("https://api.descope.com");
when(client.getFgaCacheUri()).thenReturn("https://cache.example.com/");

try (MockedStatic<ApiProxyBuilder> mockedStatic = Mockito.mockStatic(ApiProxyBuilder.class)) {
mockedStatic.when(() -> ApiProxyBuilder.buildProxy(any(), any())).thenReturn(apiProxy);

fgaService.check(Arrays.asList(new FGARelation("doc1", "doc", "viewer", "user1", "user")));
fgaService.dryRunSchema(new FGASchema("model AuthZ 1.0\ntype user"));

ArgumentCaptor<URI> uriCaptor = ArgumentCaptor.forClass(URI.class);
verify(apiProxy, times(2)).post(uriCaptor.capture(), any(), any());
assertEquals("https://cache.example.com/v1/mgmt/fga/check", uriCaptor.getAllValues().get(0).toString());
assertEquals("https://api.descope.com/v1/mgmt/fga/schema/dryrun",
uriCaptor.getAllValues().get(1).toString());
}
}

@Test
void testFgaCacheRoutingUsesBaseUrlWhenNotConfigured() throws Exception {
when(client.getUri()).thenReturn("https://api.descope.com");

try (MockedStatic<ApiProxyBuilder> mockedStatic = Mockito.mockStatic(ApiProxyBuilder.class)) {
mockedStatic.when(() -> ApiProxyBuilder.buildProxy(any(), any())).thenReturn(apiProxy);

fgaService.check(Arrays.asList(new FGARelation("doc1", "doc", "viewer", "user1", "user")));

ArgumentCaptor<URI> uriCaptor = ArgumentCaptor.forClass(URI.class);
verify(apiProxy).post(uriCaptor.capture(), any(), any());
assertEquals("https://api.descope.com/v1/mgmt/fga/check", uriCaptor.getValue().toString());
}
}

@Test
void testSaveResourcesDetails_Success() throws Exception {
List<FGAResourceDetails> details = Arrays.asList(
Expand Down