diff --git a/Makefile b/Makefile index e2368e62..b2c30e12 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ # Main config -OPENFGA_DOCKER_TAG = latest +OPENFGA_DOCKER_TAG = v1.4.0-rc1 OPEN_API_URL = https://raw.githubusercontent.com/openfga/api/main/docs/openapiv2/apidocs.swagger.json OPENAPI_GENERATOR_CLI_DOCKER_TAG = v6.4.0 NODE_DOCKER_TAG = 18-alpine diff --git a/config/clients/java/config.overrides.json b/config/clients/java/config.overrides.json index 3ec726be..0897a7d5 100644 --- a/config/clients/java/config.overrides.json +++ b/config/clients/java/config.overrides.json @@ -31,6 +31,10 @@ "allowUnicodeIdentifiers": true, "caseInsensitiveResponseHeaders": true, "files": { + "auth-model.json" : { + "destinationFilename": "src/test-integration/resources/auth-model.json", + "templateType": "SupportingFiles" + }, "build.gradle.mustache" : { "destinationFilename": "build.gradle", "templateType": "SupportingFiles" @@ -123,10 +127,18 @@ "destinationFilename": "src/main/java/dev/openfga/sdk/api/client/ClientReadResponse.java", "templateType": "SupportingFiles" }, + "client-ClientRelationshipCondition.java.mustache" : { + "destinationFilename": "src/main/java/dev/openfga/sdk/api/client/ClientRelationshipCondition.java", + "templateType": "SupportingFiles" + }, "client-ClientTupleKey.java.mustache" : { "destinationFilename": "src/main/java/dev/openfga/sdk/api/client/ClientTupleKey.java", "templateType": "SupportingFiles" }, + "client-ClientTupleKeyWithoutCondition.java.mustache" : { + "destinationFilename": "src/main/java/dev/openfga/sdk/api/client/ClientTupleKeyWithoutCondition.java", + "templateType": "SupportingFiles" + }, "client-ClientWriteAssertionsResponse.java.mustache" : { "destinationFilename": "src/main/java/dev/openfga/sdk/api/client/ClientWriteAssertionsResponse.java", "templateType": "SupportingFiles" diff --git a/config/clients/java/template/.github/dependabot.yaml b/config/clients/java/template/.github/dependabot.yaml new file mode 100644 index 00000000..5a916f67 --- /dev/null +++ b/config/clients/java/template/.github/dependabot.yaml @@ -0,0 +1,18 @@ +version: 2 +updates: + - package-ecosystem: "gradle" + directory: "/" + schedule: + interval: "monthly" + groups: + dependencies: + patterns: + - "*" + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" + groups: + dependencies: + patterns: + - "*" diff --git a/config/clients/java/template/OpenFgaApiIntegrationTest.java.mustache b/config/clients/java/template/OpenFgaApiIntegrationTest.java.mustache index c172204f..0fb562b3 100644 --- a/config/clients/java/template/OpenFgaApiIntegrationTest.java.mustache +++ b/config/clients/java/template/OpenFgaApiIntegrationTest.java.mustache @@ -6,29 +6,40 @@ import static org.junit.jupiter.api.Assertions.*; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import {{invokerPackage}}.*; -import {{modelPackage}}.*; import {{configPackage}}.*; -import dev.openfga.errors.ApiException; +import {{modelPackage}}.*; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; import java.net.http.HttpClient; import java.util.List; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestInstance.Lifecycle; +@TestInstance(Lifecycle.PER_CLASS) public class OpenFgaApiIntegrationTest { private static final ObjectMapper mapper = new ObjectMapper().findAndRegisterModules(); - private static final String DEFAULT_AUTH_MODEL = - "{\"schema_version\":\"1.1\",\"type_definitions\":[{\"type\":\"user\"},{\"type\":\"document\",\"relations\":{\"reader\":{\"this\":{}},\"writer\":{\"this\":{}},\"owner\":{\"this\":{}}},\"metadata\":{\"relations\":{\"reader\":{\"directly_related_user_types\":[{\"type\":\"user\"}]},\"writer\":{\"directly_related_user_types\":[{\"type\":\"user\"}]},\"owner\":{\"directly_related_user_types\":[{\"type\":\"user\"}]}}}}]}"; private static final String DEFAULT_USER = "user:81684243-9356-4421-8fbf-a4f8d36aa31b"; private static final String DEFAULT_DOC = "document:2021-budget"; - public static final TupleKey DEFAULT_TUPLE_KEY = + private static final TupleKey DEFAULT_TUPLE_KEY = new TupleKey().user(DEFAULT_USER).relation("reader")._object(DEFAULT_DOC); - public static final List DEFAULT_TUPLE_KEYS = List.of(DEFAULT_TUPLE_KEY); + private static final List DEFAULT_TUPLE_KEYS = List.of(DEFAULT_TUPLE_KEY); + private String authModelJson; private OpenFgaApi api; + @BeforeAll + public void loadAuthModelJson() throws IOException { + authModelJson = Files.readString(Paths.get("src", "test-integration", "resources", "auth-model.json")); + } + @BeforeEach public void initializeApi() throws Exception { + System.setProperty("HttpRequestAttempt.debug-logging", "enable"); + Configuration apiConfig = new Configuration().apiUrl("http://localhost:8080"); api = new OpenFgaApi(apiConfig); } @@ -112,7 +123,7 @@ public class OpenFgaApiIntegrationTest { assertEquals(authModelId, authModel.getId()); String typeDefsJson = mapper.writeValueAsString(authModel.getTypeDefinitions()); assertEquals( - "[{\"type\":\"user\",\"relations\":{},\"metadata\":null},{\"type\":\"document\",\"relations\":{\"owner\":{\"this\":{},\"computedUserset\":null,\"tupleToUserset\":null,\"union\":null,\"intersection\":null,\"difference\":null},\"reader\":{\"this\":{},\"computedUserset\":null,\"tupleToUserset\":null,\"union\":null,\"intersection\":null,\"difference\":null},\"writer\":{\"this\":{},\"computedUserset\":null,\"tupleToUserset\":null,\"union\":null,\"intersection\":null,\"difference\":null}},\"metadata\":{\"relations\":{\"owner\":{\"directly_related_user_types\":[{\"type\":\"user\",\"relation\":null,\"wildcard\":null}]},\"reader\":{\"directly_related_user_types\":[{\"type\":\"user\",\"relation\":null,\"wildcard\":null}]},\"writer\":{\"directly_related_user_types\":[{\"type\":\"user\",\"relation\":null,\"wildcard\":null}]}}}}]", + "[{\"type\":\"user\",\"relations\":{},\"metadata\":null},{\"type\":\"document\",\"relations\":{\"owner\":{\"this\":{},\"computedUserset\":null,\"tupleToUserset\":null,\"union\":null,\"intersection\":null,\"difference\":null},\"reader\":{\"this\":{},\"computedUserset\":null,\"tupleToUserset\":null,\"union\":null,\"intersection\":null,\"difference\":null},\"writer\":{\"this\":{},\"computedUserset\":null,\"tupleToUserset\":null,\"union\":null,\"intersection\":null,\"difference\":null}},\"metadata\":{\"relations\":{\"conditional_reader\":{\"directly_related_user_types\":[{\"type\":\"user\",\"relation\":null,\"wildcard\":null,\"condition\":\"name_starts_with_a\"}]},\"owner\":{\"directly_related_user_types\":[{\"type\":\"user\",\"relation\":null,\"wildcard\":null,\"condition\":\"\"}]},\"reader\":{\"directly_related_user_types\":[{\"type\":\"user\",\"relation\":null,\"wildcard\":null,\"condition\":\"\"}]},\"writer\":{\"directly_related_user_types\":[{\"type\":\"user\",\"relation\":null,\"wildcard\":null,\"condition\":\"\"}]}}}}]", typeDefsJson); } @@ -136,7 +147,7 @@ public class OpenFgaApiIntegrationTest { String typeDefsJson = mapper.writeValueAsString(authModel.getTypeDefinitions()); assertEquals( - "[{\"type\":\"user\",\"relations\":{},\"metadata\":null},{\"type\":\"document\",\"relations\":{\"owner\":{\"this\":{},\"computedUserset\":null,\"tupleToUserset\":null,\"union\":null,\"intersection\":null,\"difference\":null},\"reader\":{\"this\":{},\"computedUserset\":null,\"tupleToUserset\":null,\"union\":null,\"intersection\":null,\"difference\":null},\"writer\":{\"this\":{},\"computedUserset\":null,\"tupleToUserset\":null,\"union\":null,\"intersection\":null,\"difference\":null}},\"metadata\":{\"relations\":{\"owner\":{\"directly_related_user_types\":[{\"type\":\"user\",\"relation\":null,\"wildcard\":null}]},\"reader\":{\"directly_related_user_types\":[{\"type\":\"user\",\"relation\":null,\"wildcard\":null}]},\"writer\":{\"directly_related_user_types\":[{\"type\":\"user\",\"relation\":null,\"wildcard\":null}]}}}}]", + "[{\"type\":\"user\",\"relations\":{},\"metadata\":null},{\"type\":\"document\",\"relations\":{\"owner\":{\"this\":{},\"computedUserset\":null,\"tupleToUserset\":null,\"union\":null,\"intersection\":null,\"difference\":null},\"reader\":{\"this\":{},\"computedUserset\":null,\"tupleToUserset\":null,\"union\":null,\"intersection\":null,\"difference\":null},\"writer\":{\"this\":{},\"computedUserset\":null,\"tupleToUserset\":null,\"union\":null,\"intersection\":null,\"difference\":null}},\"metadata\":{\"relations\":{\"conditional_reader\":{\"directly_related_user_types\":[{\"type\":\"user\",\"relation\":null,\"wildcard\":null,\"condition\":\"name_starts_with_a\"}]},\"owner\":{\"directly_related_user_types\":[{\"type\":\"user\",\"relation\":null,\"wildcard\":null,\"condition\":\"\"}]},\"reader\":{\"directly_related_user_types\":[{\"type\":\"user\",\"relation\":null,\"wildcard\":null,\"condition\":\"\"}]},\"writer\":{\"directly_related_user_types\":[{\"type\":\"user\",\"relation\":null,\"wildcard\":null,\"condition\":\"\"}]}}}}]", typeDefsJson); } catch (JsonProcessingException ex) { assertNull(ex); @@ -149,8 +160,7 @@ public class OpenFgaApiIntegrationTest { // Given String storeName = thisTestName(); String storeId = createStore(storeName); - WriteAuthorizationModelRequest request = - mapper.readValue(DEFAULT_AUTH_MODEL, WriteAuthorizationModelRequest.class); + WriteAuthorizationModelRequest request = mapper.readValue(authModelJson, WriteAuthorizationModelRequest.class); // When WriteAuthorizationModelResponse response = @@ -168,9 +178,9 @@ public class OpenFgaApiIntegrationTest { String storeName = thisTestName(); String storeId = createStore(storeName); String _authModelId = writeAuthModel(storeId); - WriteRequest writeRequest = new WriteRequest().writes(new TupleKeys().tupleKeys(List.of(DEFAULT_TUPLE_KEY))); + WriteRequest writeRequest = new WriteRequest().writes(new WriteRequestWrites().tupleKeys(List.of(DEFAULT_TUPLE_KEY))); ReadRequest readRequest = - new ReadRequest().tupleKey(new TupleKey().user(DEFAULT_USER)._object(DEFAULT_DOC)); + new ReadRequest().tupleKey(new ReadRequestTupleKey().user(DEFAULT_USER)._object(DEFAULT_DOC)); // When api.write(storeId, writeRequest).get(); @@ -189,9 +199,9 @@ public class OpenFgaApiIntegrationTest { String storeName = thisTestName(); String storeId = createStore(storeName); String _authModelId = writeAuthModel(storeId); - WriteRequest writeRequest = new WriteRequest().writes(new TupleKeys().tupleKeys(DEFAULT_TUPLE_KEYS)); + WriteRequest writeRequest = new WriteRequest().writes(new WriteRequestWrites().tupleKeys(DEFAULT_TUPLE_KEYS)); CheckRequest checkRequest = new CheckRequest() - .tupleKey(new TupleKey().user(DEFAULT_USER).relation("reader")._object(DEFAULT_DOC)); + .tupleKey(new CheckRequestTupleKey().user(DEFAULT_USER).relation("reader")._object(DEFAULT_DOC)); // When api.write(storeId, writeRequest).get(); @@ -207,9 +217,9 @@ public class OpenFgaApiIntegrationTest { String storeName = thisTestName(); String storeId = createStore(storeName); String _authModelId = writeAuthModel(storeId); - WriteRequest writeRequest = new WriteRequest().writes(new TupleKeys().tupleKeys(DEFAULT_TUPLE_KEYS)); + WriteRequest writeRequest = new WriteRequest().writes(new WriteRequestWrites().tupleKeys(DEFAULT_TUPLE_KEYS)); ExpandRequest expandRequest = - new ExpandRequest().tupleKey(new TupleKey()._object(DEFAULT_DOC).relation("reader")); + new ExpandRequest().tupleKey(new ExpandRequestTupleKey()._object(DEFAULT_DOC).relation("reader")); // When api.write(storeId, writeRequest).get(); @@ -229,7 +239,7 @@ public class OpenFgaApiIntegrationTest { String storeName = thisTestName(); String storeId = createStore(storeName); String _authModelId = writeAuthModel(storeId); - WriteRequest writeRequest = new WriteRequest().writes(new TupleKeys().tupleKeys(DEFAULT_TUPLE_KEYS)); + WriteRequest writeRequest = new WriteRequest().writes(new WriteRequestWrites().tupleKeys(DEFAULT_TUPLE_KEYS)); ListObjectsRequest listObjectsRequest = new ListObjectsRequest().user(DEFAULT_USER).relation("reader").type("document"); @@ -249,7 +259,7 @@ public class OpenFgaApiIntegrationTest { String storeName = thisTestName(); String storeId = createStore(storeName); String _authModelId = writeAuthModel(storeId); - WriteRequest writeRequest = new WriteRequest().writes(new TupleKeys().tupleKeys(DEFAULT_TUPLE_KEYS)); + WriteRequest writeRequest = new WriteRequest().writes(new WriteRequestWrites().tupleKeys(DEFAULT_TUPLE_KEYS)); // When api.write(storeId, writeRequest).get(); @@ -261,7 +271,7 @@ public class OpenFgaApiIntegrationTest { String firstTupleKeyJson = mapper.writeValueAsString(response.getChanges().get(0).getTupleKey()); assertEquals( - "{\"object\":\"document:2021-budget\",\"relation\":\"reader\",\"user\":\"user:81684243-9356-4421-8fbf-a4f8d36aa31b\"}", + "{\"user\":\"user:81684243-9356-4421-8fbf-a4f8d36aa31b\",\"relation\":\"reader\",\"object\":\"document:2021-budget\",\"condition\":null}", firstTupleKeyJson); } @@ -272,7 +282,12 @@ public class OpenFgaApiIntegrationTest { String storeId = createStore(storeName); String authModelId = writeAuthModel(storeId); WriteAssertionsRequest writeRequest = new WriteAssertionsRequest() - .assertions(List.of(new Assertion().tupleKey(DEFAULT_TUPLE_KEY).expectation(true))); + .assertions(List.of(new Assertion() + .tupleKey(new CheckRequestTupleKey() + .user(DEFAULT_USER) + .relation("reader") + ._object(DEFAULT_DOC)) + .expectation(true))); // When api.writeAssertions(storeId, authModelId, writeRequest).get(); @@ -282,7 +297,7 @@ public class OpenFgaApiIntegrationTest { // Then String responseJson = mapper.writeValueAsString(response.getAssertions()); assertEquals( - "[{\"tuple_key\":{\"object\":\"document:2021-budget\",\"relation\":\"reader\",\"user\":\"user:81684243-9356-4421-8fbf-a4f8d36aa31b\"},\"expectation\":true}]", + "[{\"tuple_key\":{\"user\":\"user:81684243-9356-4421-8fbf-a4f8d36aa31b\",\"relation\":\"reader\",\"object\":\"document:2021-budget\"},\"expectation\":true}]", responseJson); } @@ -303,10 +318,8 @@ public class OpenFgaApiIntegrationTest { * @return The created Authorization Model ID */ private String writeAuthModel(String storeId) throws Exception { - WriteAuthorizationModelRequest request = - mapper.readValue(DEFAULT_AUTH_MODEL, WriteAuthorizationModelRequest.class); - WriteAuthorizationModelResponse response = - api.writeAuthorizationModel(storeId, request).get().getData(); + var request = mapper.readValue(authModelJson, WriteAuthorizationModelRequest.class); + var response = api.writeAuthorizationModel(storeId, request).get().getData(); return response.getAuthorizationModelId(); } diff --git a/config/clients/java/template/OpenFgaApiTest.java.mustache b/config/clients/java/template/OpenFgaApiTest.java.mustache index 4c9b73b0..c69447c4 100644 --- a/config/clients/java/template/OpenFgaApiTest.java.mustache +++ b/config/clients/java/template/OpenFgaApiTest.java.mustache @@ -14,7 +14,9 @@ import {{configPackage}}.*; import {{errorsPackage}}.*; import java.net.http.HttpClient; import java.time.Duration; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.concurrent.ExecutionException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -44,7 +46,10 @@ public class OpenFgaApiTest { @BeforeEach public void beforeEachTest() throws Exception { + System.setProperty("HttpRequestAttempt.debug-logging", "enable"); + mockHttpClient = new HttpClientMock(); + mockHttpClient.debugOn(); mockHttpClientBuilder = mock(HttpClient.Builder.class); when(mockHttpClientBuilder.executor(any())).thenReturn(mockHttpClientBuilder); @@ -560,7 +565,7 @@ public class OpenFgaApiTest { // Given String postUrl = "https://localhost/stores/01YCP46JKYM8FJCQ37NMBYHE5X/authorization-models"; String expectedBody = - "{\"type_definitions\":[{\"type\":\"document\",\"relations\":{},\"metadata\":null}],\"schema_version\":\"1.1\"}"; + "{\"type_definitions\":[{\"type\":\"document\",\"relations\":{},\"metadata\":null}],\"schema_version\":\"1.1\",\"conditions\":{}}"; String responseBody = String.format("{\"authorization_model_id\":\"%s\"}", DEFAULT_AUTH_MODEL_ID); mockHttpClient.onPost(postUrl).withBody(is(expectedBody)).doReturn(201, responseBody); WriteAuthorizationModelRequest request = new WriteAuthorizationModelRequest() @@ -793,8 +798,8 @@ public class OpenFgaApiTest { // Given String getPath = "https://localhost/stores/01YCP46JKYM8FJCQ37NMBYHE5X/changes"; String responseBody = String.format( - "{\"changes\":[{\"tuple_key\":{\"object\":\"%s\",\"relation\":\"%s\",\"user\":\"%s\"}}]}", - DEFAULT_OBJECT, DEFAULT_RELATION, DEFAULT_USER); + "{\"changes\":[{\"tuple_key\":{\"user\":\"%s\",\"relation\":\"%s\",\"object\":\"%s\"}}]}", + DEFAULT_USER, DEFAULT_RELATION, DEFAULT_OBJECT); mockHttpClient.onGet(getPath).doReturn(200, responseBody); String type = null; // Input is optional Integer pageSize = null; // Input is optional @@ -909,14 +914,57 @@ public class OpenFgaApiTest { // Given String postUrl = "https://localhost/stores/01YCP46JKYM8FJCQ37NMBYHE5X/read"; String expectedBody = String.format( - "{\"tuple_key\":{\"object\":\"%s\",\"relation\":\"%s\",\"user\":\"%s\"},\"page_size\":null,\"continuation_token\":null}", - DEFAULT_OBJECT, DEFAULT_RELATION, DEFAULT_USER); + "{\"tuple_key\":{\"user\":\"%s\",\"relation\":\"%s\",\"object\":\"%s\"},\"page_size\":null,\"continuation_token\":null}", + DEFAULT_USER, DEFAULT_RELATION, DEFAULT_OBJECT); String responseBody = String.format( "{\"tuples\":[{\"key\":{\"user\":\"%s\",\"relation\":\"%s\",\"object\":\"%s\"}}]}", DEFAULT_USER, DEFAULT_RELATION, DEFAULT_OBJECT); mockHttpClient.onPost(postUrl).withBody(is(expectedBody)).doReturn(200, responseBody); ReadRequest request = new ReadRequest() - .tupleKey(new TupleKey() + .tupleKey(new ReadRequestTupleKey() + ._object(DEFAULT_OBJECT) + .relation(DEFAULT_RELATION) + .user(DEFAULT_USER)); + + // When + var response = fga.read(DEFAULT_STORE_ID, request).get(); + + // Then + mockHttpClient.verify().post(postUrl).withBody(is(expectedBody)).called(1); + assertNotNull(response.getData()); + assertNotNull(response.getData().getTuples()); + assertEquals(1, response.getData().getTuples().size()); + var key = response.getData().getTuples().get(0).getKey(); + assertNotNull(key); + assertEquals(DEFAULT_USER, key.getUser()); + assertEquals(DEFAULT_RELATION, key.getRelation()); + assertEquals(DEFAULT_OBJECT, key.getObject()); + } + + @Test + public void read_complexContext() throws Exception { + // Given + String postUrl = "https://localhost/stores/01YCP46JKYM8FJCQ37NMBYHE5X/read"; + String expectedBody = String.format( + "{\"tuple_key\":{\"user\":\"%s\",\"relation\":\"%s\",\"object\":\"%s\"},\"page_size\":null,\"continuation_token\":null}", + DEFAULT_USER, DEFAULT_RELATION, DEFAULT_OBJECT); + String responseBody = String.format( + "{\"tuples\":[{\"key\":{\"user\":\"%s\",\"relation\":\"%s\",\"object\":\"%s\"," + + "\"condition\":{\"context\":{" + + " \"num\":1," + + " \"str\":\"banana\"," + + " \"list\":[1, \"banana\", [], {}]," + + " \"obj\":{" + + " \"num\":1," + + " \"str\":\"banana\"," + + " \"list\":[]," + + " \"obj\": {}" + + " }" + + "}}}}]}", + DEFAULT_USER, DEFAULT_RELATION, DEFAULT_OBJECT); + mockHttpClient.onPost(postUrl).withBody(is(expectedBody)).doReturn(200, responseBody); + ReadRequest request = new ReadRequest() + .tupleKey(new ReadRequestTupleKey() ._object(DEFAULT_OBJECT) .relation(DEFAULT_RELATION) .user(DEFAULT_USER)); @@ -934,6 +982,19 @@ public class OpenFgaApiTest { assertEquals(DEFAULT_USER, key.getUser()); assertEquals(DEFAULT_RELATION, key.getRelation()); assertEquals(DEFAULT_OBJECT, key.getObject()); + + // The below is subject to change. + assertNotNull(key.getCondition()); + var context = key.getCondition().getContext(); + assertNotNull(context); + var contextMap = assertInstanceOf(Map.class, context); + assertEquals(1, contextMap.get("num")); + assertEquals("banana", contextMap.get("str")); + assertEquals(List.of(1, "banana", List.of(), Map.of()), contextMap.get("list")); + assertEquals(Map.of("num", 1, + "str", "banana", + "list", List.of(), + "obj", Map.of()), contextMap.get("obj")); } @Test @@ -1031,19 +1092,19 @@ public class OpenFgaApiTest { // Given String postPath = "https://localhost/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write"; String expectedBody = String.format( - "{\"writes\":{\"tuple_keys\":[{\"object\":\"%s\",\"relation\":\"%s\",\"user\":\"%s\"}]},\"deletes\":null,\"authorization_model_id\":\"%s\"}", - DEFAULT_OBJECT, DEFAULT_RELATION, DEFAULT_USER, DEFAULT_AUTH_MODEL_ID); + "{\"writes\":{\"tuple_keys\":[{\"user\":\"%s\",\"relation\":\"%s\",\"object\":\"%s\",\"condition\":null}]},\"deletes\":null,\"authorization_model_id\":\"%s\"}", + DEFAULT_USER, DEFAULT_RELATION, DEFAULT_OBJECT, DEFAULT_AUTH_MODEL_ID); mockHttpClient.onPost(postPath).withBody(is(expectedBody)).doReturn(200, EMPTY_RESPONSE_BODY); WriteRequest request = new WriteRequest() .authorizationModelId(DEFAULT_AUTH_MODEL_ID) - .writes(new TupleKeys() + .writes(new WriteRequestWrites() .tupleKeys(List.of(new TupleKey() ._object(DEFAULT_OBJECT) .relation(DEFAULT_RELATION) .user(DEFAULT_USER)))); // When - fga.write(DEFAULT_STORE_ID, request); + fga.write(DEFAULT_STORE_ID, request).get(); // Then mockHttpClient.verify().post(postPath).withBody(is(expectedBody)).called(1); @@ -1057,19 +1118,112 @@ public class OpenFgaApiTest { // Given String postPath = "https://localhost/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write"; String expectedBody = String.format( - "{\"writes\":null,\"deletes\":{\"tuple_keys\":[{\"object\":\"%s\",\"relation\":\"%s\",\"user\":\"%s\"}]},\"authorization_model_id\":\"%s\"}", - DEFAULT_OBJECT, DEFAULT_RELATION, DEFAULT_USER, DEFAULT_AUTH_MODEL_ID); + "{\"writes\":null,\"deletes\":{\"tuple_keys\":[{\"user\":\"%s\",\"relation\":\"%s\",\"object\":\"%s\"}]},\"authorization_model_id\":\"%s\"}", + DEFAULT_USER, DEFAULT_RELATION, DEFAULT_OBJECT, DEFAULT_AUTH_MODEL_ID); mockHttpClient.onPost(postPath).withBody(is(expectedBody)).doReturn(200, EMPTY_RESPONSE_BODY); WriteRequest request = new WriteRequest() .authorizationModelId(DEFAULT_AUTH_MODEL_ID) - .deletes(new TupleKeys() - .tupleKeys(List.of(new TupleKey() + .deletes(new WriteRequestDeletes() + .tupleKeys(List.of(new TupleKeyWithoutCondition() ._object(DEFAULT_OBJECT) .relation(DEFAULT_RELATION) .user(DEFAULT_USER)))); // When - fga.write(DEFAULT_STORE_ID, request); + fga.write(DEFAULT_STORE_ID, request).get(); + + // Then + mockHttpClient.verify().post(postPath).withBody(is(expectedBody)).called(1); + } + + @Test + public void writeWithContext_map() throws Exception { + // Given + String postPath = "https://localhost/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write"; + String expectedBody = String.format( + "{\"writes\":{\"tuple_keys\":[{\"user\":\"%s\",\"relation\":\"%s\",\"object\":\"%s\",\"condition\":{\"name\":\"conditionName\",\"context\":{\"num\":1,\"str\":\"banana\",\"list\":[],\"obj\":{}}}}]},\"deletes\":null,\"authorization_model_id\":\"%s\"}", + DEFAULT_USER, DEFAULT_RELATION, DEFAULT_OBJECT, DEFAULT_AUTH_MODEL_ID); + mockHttpClient.onPost(postPath).withBody(is(expectedBody)).doReturn(200, EMPTY_RESPONSE_BODY); + var context = new LinkedHashMap<>(); + context.put("num", 1); + context.put("str", "banana"); + context.put("list", List.of()); + context.put("obj", new LinkedHashMap<>()); + WriteRequest request = new WriteRequest() + .authorizationModelId(DEFAULT_AUTH_MODEL_ID) + .writes(new WriteRequestWrites() + .tupleKeys(List.of(new TupleKey() + ._object(DEFAULT_OBJECT) + .relation(DEFAULT_RELATION) + .user(DEFAULT_USER) + .condition(new RelationshipCondition() + .name("conditionName") + .context(context))))); + + // When + fga.write(DEFAULT_STORE_ID, request).get(); + + // Then + mockHttpClient.verify().post(postPath).withBody(is(expectedBody)).called(1); + } + + @Test + public void writeWithContext_modeledObj() throws Exception { + // Given + + String postPath = "https://localhost/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write"; + String expectedBody = String.format( + "{\"writes\":{\"tuple_keys\":[{\"user\":\"%s\",\"relation\":\"%s\",\"object\":\"%s\",\"condition\":{\"name\":\"conditionName\",\"context\":{\"num\":1,\"str\":\"apple\",\"list\":[2,\"banana\",[],{\"num\":3,\"str\":\"cupcake\",\"list\":null,\"obj\":null}],\"obj\":{\"num\":4,\"str\":\"dolphin\",\"list\":null,\"obj\":null}}}}]},\"deletes\":null,\"authorization_model_id\":\"%s\"}", + DEFAULT_USER, DEFAULT_RELATION, DEFAULT_OBJECT, DEFAULT_AUTH_MODEL_ID); + mockHttpClient.onPost(postPath).withBody(is(expectedBody)).doReturn(200, EMPTY_RESPONSE_BODY); + + class TestObj { + int num; + String str; + List list; + Object obj; + + public int getNum() { + return num; + } + + public String getStr() { + return str; + } + + public List getList() { + return list; + } + + public Object getObj() { + return obj; + } + } + var obj = new TestObj(); + obj.num = 1; + obj.str = "apple"; + var objInList = new TestObj(); + obj.list = List.of(2, "banana", List.of(), objInList); + objInList.num = 3; + objInList.str = "cupcake"; + var objInObj = new TestObj(); + obj.obj = objInObj; + objInObj.num = 4; + objInObj.str = "dolphin"; + + WriteRequest request = new WriteRequest() + .authorizationModelId(DEFAULT_AUTH_MODEL_ID) + .writes(new WriteRequestWrites() + .tupleKeys(List.of(new TupleKey() + ._object(DEFAULT_OBJECT) + .relation(DEFAULT_RELATION) + .user(DEFAULT_USER) + .condition(new RelationshipCondition() + .name("conditionName") + .context(obj))))); + + // When + fga.write(DEFAULT_STORE_ID, request).get(); // Then mockHttpClient.verify().post(postPath).withBody(is(expectedBody)).called(1); @@ -1171,11 +1325,11 @@ public class OpenFgaApiTest { // Given String postPath = "https://localhost/stores/01YCP46JKYM8FJCQ37NMBYHE5X/check"; String expectedBody = String.format( - "{\"tuple_key\":{\"object\":\"%s\",\"relation\":\"%s\",\"user\":\"%s\"},\"contextual_tuples\":{\"tuple_keys\":[]},\"authorization_model_id\":\"01G5JAVJ41T49E9TT3SKVS7X1J\",\"trace\":null}", - DEFAULT_OBJECT, DEFAULT_RELATION, DEFAULT_USER); + "{\"tuple_key\":{\"user\":\"%s\",\"relation\":\"%s\",\"object\":\"%s\"},\"contextual_tuples\":{\"tuple_keys\":[]},\"authorization_model_id\":\"01G5JAVJ41T49E9TT3SKVS7X1J\",\"trace\":null,\"context\":null}", + DEFAULT_USER, DEFAULT_RELATION, DEFAULT_OBJECT); mockHttpClient.onPost(postPath).withBody(is(expectedBody)).doReturn(200, "{\"allowed\":true}"); CheckRequest request = new CheckRequest() - .tupleKey(new TupleKey() + .tupleKey(new CheckRequestTupleKey() ._object(DEFAULT_OBJECT) .relation(DEFAULT_RELATION) .user(DEFAULT_USER)) @@ -1289,18 +1443,15 @@ public class OpenFgaApiTest { // Given String postPath = "https://localhost/stores/01YCP46JKYM8FJCQ37NMBYHE5X/expand"; String expectedBody = String.format( - "{\"tuple_key\":{\"object\":\"%s\",\"relation\":\"%s\",\"user\":\"%s\"},\"authorization_model_id\":\"%s\"}", - DEFAULT_OBJECT, DEFAULT_RELATION, DEFAULT_USER, DEFAULT_AUTH_MODEL_ID); + "{\"tuple_key\":{\"relation\":\"%s\",\"object\":\"%s\"},\"authorization_model_id\":\"%s\"}", + DEFAULT_RELATION, DEFAULT_OBJECT, DEFAULT_AUTH_MODEL_ID); String responseBody = String.format( "{\"tree\":{\"root\":{\"union\":{\"nodes\":[{\"leaf\":{\"users\":{\"users\":[\"%s\"]}}}]}}}}", DEFAULT_USER); mockHttpClient.onPost(postPath).withBody(is(expectedBody)).doReturn(200, responseBody); ExpandRequest request = new ExpandRequest() .authorizationModelId(DEFAULT_AUTH_MODEL_ID) - .tupleKey(new TupleKey() - ._object(DEFAULT_OBJECT) - .relation(DEFAULT_RELATION) - .user(DEFAULT_USER)); + .tupleKey(new ExpandRequestTupleKey()._object(DEFAULT_OBJECT).relation(DEFAULT_RELATION)); // When var response = fga.expand(DEFAULT_STORE_ID, request).get(); @@ -1421,7 +1572,7 @@ public class OpenFgaApiTest { // Given String postPath = "https://localhost/stores/01YCP46JKYM8FJCQ37NMBYHE5X/list-objects"; String expectedBody = String.format( - "{\"authorization_model_id\":\"%s\",\"type\":null,\"relation\":\"%s\",\"user\":\"%s\",\"contextual_tuples\":null}", + "{\"authorization_model_id\":\"%s\",\"type\":null,\"relation\":\"%s\",\"user\":\"%s\",\"contextual_tuples\":null,\"context\":null}", DEFAULT_AUTH_MODEL_ID, DEFAULT_RELATION, DEFAULT_USER); mockHttpClient .onPost(postPath) @@ -1537,8 +1688,8 @@ public class OpenFgaApiTest { // Given String getUrl = "https://localhost/stores/01YCP46JKYM8FJCQ37NMBYHE5X/assertions/01G5JAVJ41T49E9TT3SKVS7X1J"; String responseBody = String.format( - "{\"assertions\":[{\"tuple_key\":{\"object\":\"%s\",\"relation\":\"%s\",\"user\":\"%s\"},\"expectation\":true}]}", - DEFAULT_OBJECT, DEFAULT_RELATION, DEFAULT_USER); + "{\"assertions\":[{\"tuple_key\":{\"user\":\"%s\",\"relation\":\"%s\",\"object\":\"%s\"},\"expectation\":true}]}", + DEFAULT_USER, DEFAULT_RELATION, DEFAULT_OBJECT); mockHttpClient.onGet(getUrl).doReturn(200, responseBody); // When @@ -1657,12 +1808,12 @@ public class OpenFgaApiTest { // Given String putUrl = "https://localhost/stores/01YCP46JKYM8FJCQ37NMBYHE5X/assertions/01G5JAVJ41T49E9TT3SKVS7X1J"; String expectedBody = String.format( - "{\"assertions\":[{\"tuple_key\":{\"object\":\"%s\",\"relation\":\"%s\",\"user\":\"%s\"},\"expectation\":true}]}", - DEFAULT_OBJECT, DEFAULT_RELATION, DEFAULT_USER); + "{\"assertions\":[{\"tuple_key\":{\"user\":\"%s\",\"relation\":\"%s\",\"object\":\"%s\"},\"expectation\":true}]}", + DEFAULT_USER, DEFAULT_RELATION, DEFAULT_OBJECT); mockHttpClient.onPut(putUrl).withBody(is(expectedBody)).doReturn(200, EMPTY_RESPONSE_BODY); WriteAssertionsRequest request = new WriteAssertionsRequest() .assertions(List.of(new Assertion() - .tupleKey(new TupleKey() + .tupleKey(new CheckRequestTupleKey() ._object(DEFAULT_OBJECT) .relation(DEFAULT_RELATION) .user(DEFAULT_USER)) diff --git a/config/clients/java/template/README_calling_api.mustache b/config/clients/java/template/README_calling_api.mustache index 67e6624e..716240ec 100644 --- a/config/clients/java/template/README_calling_api.mustache +++ b/config/clients/java/template/README_calling_api.mustache @@ -277,7 +277,7 @@ var request = new ClientWriteRequest() ._object("document:budget") )) .deletes(List.of( - new ClientTupleKey() + new ClientTupleKeyWithoutCondition() .user("user:81684243-9356-4421-8fbf-a4f8d36aa31b") .relation("writer") ._object("document:roadmap") diff --git a/config/clients/java/template/auth-model.json b/config/clients/java/template/auth-model.json new file mode 100644 index 00000000..3ef492c5 --- /dev/null +++ b/config/clients/java/template/auth-model.json @@ -0,0 +1,67 @@ +{ + "schema_version": "1.1", + "type_definitions": [ + { + "type": "user" + }, + { + "type": "document", + "relations": { + "reader": { + "this": {} + }, + "writer": { + "this": {} + }, + "owner": { + "this": {} + } + }, + "metadata": { + "relations": { + "reader": { + "directly_related_user_types": [ + { + "type": "user" + } + ] + }, + "writer": { + "directly_related_user_types": [ + { + "type": "user" + } + ] + }, + "owner": { + "directly_related_user_types": [ + { + "type": "user" + } + ] + }, + "conditional_reader": { + "directly_related_user_types": [ + { + "condition": "name_starts_with_a", + "type": "user" + } + ] + } + } + } + } + ], + "conditions": { + "name_starts_with_a": { + "name": "name_starts_with_a", + "expression": "name.startsWith(\"a\")", + "parameters": { + "name": { + "type_name": "TYPE_NAME_STRING" + } + } + } + } + } + \ No newline at end of file diff --git a/config/clients/java/template/client-ClientAssertion.java.mustache b/config/clients/java/template/client-ClientAssertion.java.mustache index 8624fcd2..e3ded689 100644 --- a/config/clients/java/template/client-ClientAssertion.java.mustache +++ b/config/clients/java/template/client-ClientAssertion.java.mustache @@ -2,8 +2,7 @@ package {{invokerPackage}}; import {{modelPackage}}.Assertion; -import {{modelPackage}}.TupleKey; - +import {{modelPackage}}.CheckRequestTupleKey; import java.util.List; import java.util.stream.Collectors; @@ -62,7 +61,7 @@ public class ClientAssertion { } public Assertion asAssertion() { - TupleKey tupleKey = new TupleKey().user(user).relation(relation)._object(_object); + var tupleKey = new CheckRequestTupleKey().user(user).relation(relation)._object(_object); return new Assertion().tupleKey(tupleKey).expectation(expectation); } diff --git a/config/clients/java/template/client-ClientCheckRequest.java.mustache b/config/clients/java/template/client-ClientCheckRequest.java.mustache index ff2da5b6..1d477745 100644 --- a/config/clients/java/template/client-ClientCheckRequest.java.mustache +++ b/config/clients/java/template/client-ClientCheckRequest.java.mustache @@ -1,6 +1,7 @@ {{>licenseInfo}} package {{invokerPackage}}; +import dev.openfga.sdk.api.model.CheckRequestTupleKey; import java.util.List; public class ClientCheckRequest { @@ -9,6 +10,10 @@ public class ClientCheckRequest { private String _object; private List contextualTuples; + public CheckRequestTupleKey asCheckRequestTupleKey() { + return new CheckRequestTupleKey().user(user).relation(relation)._object(_object); + } + public ClientCheckRequest _object(String _object) { this._object = _object; return this; diff --git a/config/clients/java/template/client-ClientRelationshipCondition.java.mustache b/config/clients/java/template/client-ClientRelationshipCondition.java.mustache new file mode 100644 index 00000000..bc6ed094 --- /dev/null +++ b/config/clients/java/template/client-ClientRelationshipCondition.java.mustache @@ -0,0 +1,31 @@ +{{>licenseInfo}} +package {{invokerPackage}}; + +import {{modelPackage}}.RelationshipCondition; + +public class ClientRelationshipCondition { + private String name; + private Object context; + + public ClientRelationshipCondition name(String name) { + this.name = name; + return this; + } + + public String getName() { + return name; + } + + public ClientRelationshipCondition context(Object context) { + this.context = context; + return this; + } + + public Object getContext() { + return context; + } + + public RelationshipCondition asRelationshipCondition() { + return new RelationshipCondition().name(name).context(context); + } +} diff --git a/config/clients/java/template/client-ClientTupleKey.java.mustache b/config/clients/java/template/client-ClientTupleKey.java.mustache index c380b0ed..3b1a826a 100644 --- a/config/clients/java/template/client-ClientTupleKey.java.mustache +++ b/config/clients/java/template/client-ClientTupleKey.java.mustache @@ -3,76 +3,63 @@ package {{invokerPackage}}; import {{modelPackage}}.ContextualTupleKeys; import {{modelPackage}}.TupleKey; -import {{modelPackage}}.TupleKeys; -import java.util.List; -import java.util.Optional; +import {{modelPackage}}.WriteRequestWrites; +import java.util.Collection; import java.util.stream.Collectors; -public class ClientTupleKey { - private String user; - private String relation; - private String _object; +public class ClientTupleKey extends ClientTupleKeyWithoutCondition { + private ClientRelationshipCondition condition; - public ClientTupleKey _object(String _object) { - this._object = _object; + public ClientTupleKey condition(ClientRelationshipCondition condition) { + this.condition = condition; return this; } - /** - * Get _object - * @return _object - **/ - public String getObject() { - return _object; + public ClientRelationshipCondition getCondition() { + return condition; } - public ClientTupleKey relation(String relation) { - this.relation = relation; - return this; - } + public TupleKey asTupleKey() { + var tupleKey = new TupleKey().user(getUser()).relation(getRelation())._object(getObject()); - /** - * Get relation - * @return relation - **/ - public String getRelation() { - return relation; - } + if (condition != null) { + tupleKey.condition(condition.asRelationshipCondition()); + } - public ClientTupleKey user(String user) { - this.user = user; - return this; + return tupleKey; } - /** - * Get user - * @return user - **/ - public String getUser() { - return user; + public static ContextualTupleKeys asContextualTupleKeys(Collection tupleKeys) { + return new ContextualTupleKeys() + .tupleKeys(tupleKeys.stream() + .map(ClientTupleKey::asTupleKey) + .collect(Collectors.toList())); } - public TupleKey asTupleKey() { - return new TupleKey().user(user).relation(relation)._object(_object); + public static WriteRequestWrites asWriteRequestWrites(Collection tupleKeys) { + return new WriteRequestWrites() + .tupleKeys(tupleKeys.stream() + .map(ClientTupleKey::asTupleKey) + .collect(Collectors.toList())); } - public static Optional asTupleKeys(List clientTupleKeys) { - if (clientTupleKeys == null || clientTupleKeys.size() == 0) { - return Optional.empty(); - } + /* Overrides for correct typing */ - return Optional.of(new TupleKeys().tupleKeys(asListOfTupleKey(clientTupleKeys))); + @Override + public ClientTupleKey user(String user) { + super.user(user); + return this; } - public static ContextualTupleKeys asContextualTupleKeys(List clientTupleKeys) { - if (clientTupleKeys == null || clientTupleKeys.size() == 0) { - return new ContextualTupleKeys(); - } - - return new ContextualTupleKeys().tupleKeys(asListOfTupleKey(clientTupleKeys)); + @Override + public ClientTupleKey relation(String relation) { + super.relation(relation); + return this; } - private static List asListOfTupleKey(List clientTupleKeys) { - return clientTupleKeys.stream().map(ClientTupleKey::asTupleKey).collect(Collectors.toList()); + @Override + public ClientTupleKey _object(String _object) { + super._object(_object); + return this; } } diff --git a/config/clients/java/template/client-ClientTupleKeyWithoutCondition.java.mustache b/config/clients/java/template/client-ClientTupleKeyWithoutCondition.java.mustache new file mode 100644 index 00000000..fc8c597a --- /dev/null +++ b/config/clients/java/template/client-ClientTupleKeyWithoutCondition.java.mustache @@ -0,0 +1,77 @@ +{{>licenseInfo}} +package {{invokerPackage}}; + +import {{modelPackage}}.TupleKeyWithoutCondition; +import {{modelPackage}}.WriteRequestDeletes; +import java.util.Collection; +import java.util.stream.Collectors; + +public class ClientTupleKeyWithoutCondition { + private String user; + private String relation; + private String _object; + + public TupleKeyWithoutCondition asTupleKeyWithoutCondition() { + return new TupleKeyWithoutCondition().user(user).relation(relation)._object(_object); + } + + public static WriteRequestDeletes asWriteRequestDeletes(Collection tupleKeys) { + return new WriteRequestDeletes() + .tupleKeys(tupleKeys.stream() + .map(ClientTupleKeyWithoutCondition::asTupleKeyWithoutCondition) + .collect(Collectors.toList())); + } + + public ClientTupleKeyWithoutCondition _object(String _object) { + this._object = _object; + return this; + } + + /** + * Get _object + * @return _object + **/ + public String getObject() { + return _object; + } + + public ClientTupleKeyWithoutCondition relation(String relation) { + this.relation = relation; + return this; + } + + /** + * Get relation + * @return relation + **/ + public String getRelation() { + return relation; + } + + public ClientTupleKeyWithoutCondition user(String user) { + this.user = user; + return this; + } + + /** + * Get user + * @return user + **/ + public String getUser() { + return user; + } + + /** + * Adds a condition to the tuple key. + * @param condition a {@link ClientRelationshipCondition} + * @return a new {@link ClientTupleKey} with this {@link ClientTupleKeyWithoutCondition}'s + * user, relation, and object, and the passed condition. + */ + public ClientTupleKey condition(ClientRelationshipCondition condition) { + return new ClientTupleKey() + .user(user) + .relation(relation) + ._object(_object) + .condition(condition); + } +} diff --git a/config/clients/java/template/client-ClientWriteRequest.java.mustache b/config/clients/java/template/client-ClientWriteRequest.java.mustache index c065d658..a3e0f8b9 100644 --- a/config/clients/java/template/client-ClientWriteRequest.java.mustache +++ b/config/clients/java/template/client-ClientWriteRequest.java.mustache @@ -5,7 +5,7 @@ import java.util.List; public class ClientWriteRequest { private List writes; - private List deletes; + private List deletes; public static ClientWriteRequest ofWrites(List writes) { return new ClientWriteRequest().writes(writes); @@ -20,16 +20,16 @@ public class ClientWriteRequest { return writes; } - public static ClientWriteRequest ofDeletes(List deletes) { + public static ClientWriteRequest ofDeletes(List deletes) { return new ClientWriteRequest().deletes(deletes); } - public ClientWriteRequest deletes(List deletes) { + public ClientWriteRequest deletes(List deletes) { this.deletes = deletes; return this; } - public List getDeletes() { + public List getDeletes() { return deletes; } } diff --git a/config/clients/java/template/client-HttpRequestAttempt.java.mustache b/config/clients/java/template/client-HttpRequestAttempt.java.mustache index 64af3963..f2852e24 100644 --- a/config/clients/java/template/client-HttpRequestAttempt.java.mustache +++ b/config/clients/java/template/client-HttpRequestAttempt.java.mustache @@ -4,10 +4,13 @@ import static {{utilPackage}}.StringUtil.isNullOrWhitespace; import {{configPackage}}.Configuration; import {{errorsPackage}}.*; import java.io.IOException; +import java.io.PrintStream; import java.net.HttpURLConnection; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.Optional; import java.util.concurrent.*; @@ -19,6 +22,9 @@ public class HttpRequestAttempt { private final String name; private final HttpRequest request; + // Intended for only testing the OpenFGA SDK itself. + private final boolean enableDebugLogging = "enable".equals(System.getProperty("HttpRequestAttempt.debug-logging")); + public HttpRequestAttempt( HttpRequest request, String name, Class clazz, ApiClient apiClient, Configuration configuration) throws FgaInvalidParameterException { @@ -33,6 +39,11 @@ public class HttpRequestAttempt { } public CompletableFuture> attemptHttpRequest() throws ApiException { + if (enableDebugLogging) { + request.bodyPublisher() + .ifPresent(requestBodyPublisher -> + requestBodyPublisher.subscribe(new BodyLogger(System.err, "request"))); + } int retryNumber = 0; return attemptHttpRequest(apiClient.getHttpClient(), retryNumber, null); } @@ -83,4 +94,37 @@ public class HttpRequestAttempt { .executor(CompletableFuture.delayedExecutor(retryDelay.toNanos(), TimeUnit.NANOSECONDS)) .build(); } + + private static class BodyLogger implements Flow.Subscriber { + private final PrintStream out; + private final String target; + + BodyLogger(PrintStream out, String target) { + this.out = out; + this.target = target; + } + + @Override + public void onSubscribe(Flow.Subscription subscription) { + out.printf("[%s] subscribed: %s\n", this.getClass().getName(), subscription); + subscription.request(Long.MAX_VALUE); + } + + @Override + public void onNext(ByteBuffer item) { + out.printf( + "[%s] %s: %s\n", + this.getClass().getName(), target, new String(item.array(), StandardCharsets.UTF_8)); + } + + @Override + public void onError(Throwable throwable) { + out.printf("[%s] error: %s\n", this.getClass().getName(), throwable); + } + + @Override + public void onComplete() { + out.flush(); + } + } } diff --git a/config/clients/java/template/client-OpenFgaClient.java.mustache b/config/clients/java/template/client-OpenFgaClient.java.mustache index 2c28bf44..067f22b3 100644 --- a/config/clients/java/template/client-OpenFgaClient.java.mustache +++ b/config/clients/java/template/client-OpenFgaClient.java.mustache @@ -233,9 +233,10 @@ public class OpenFgaClient { ReadRequest body = new ReadRequest(); if (request != null && (request.getUser() != null || request.getRelation() != null || request.getObject() != null)) { - TupleKey tupleKey = new TupleKey(); - tupleKey.user(request.getUser()).relation(request.getRelation())._object(request.getObject()); - body.tupleKey(tupleKey); + body.tupleKey(new ReadRequestTupleKey() + .user(request.getUser()) + .relation(request.getRelation()) + ._object(request.getObject())); } if (options != null) { @@ -277,8 +278,15 @@ public class OpenFgaClient { WriteRequest body = new WriteRequest(); - ClientTupleKey.asTupleKeys(request.getWrites()).ifPresent(body::writes); - ClientTupleKey.asTupleKeys(request.getDeletes()).ifPresent(body::deletes); + var writeTuples = request.getWrites(); + if (writeTuples != null && !writeTuples.isEmpty()) { + body.writes(ClientTupleKey.asWriteRequestWrites(writeTuples)); + } + + var deleteTuples = request.getDeletes(); + if (deleteTuples != null && !deleteTuples.isEmpty()) { + body.deletes(ClientTupleKeyWithoutCondition.asWriteRequestDeletes(deleteTuples)); + } if (options != null && !isNullOrWhitespace(options.getAuthorizationModelId())) { body.authorizationModelId(options.getAuthorizationModelId()); @@ -342,15 +350,16 @@ public class OpenFgaClient { configuration.assertValid(); String storeId = configuration.getStoreIdChecked(); - var request = new WriteRequest(); - ClientTupleKey.asTupleKeys(tupleKeys).ifPresent(request::writes); + var body = new WriteRequest(); + + body.writes(ClientTupleKey.asWriteRequestWrites(tupleKeys)); String authorizationModelId = configuration.getAuthorizationModelId(); if (!isNullOrWhitespace(authorizationModelId)) { - request.authorizationModelId(authorizationModelId); + body.authorizationModelId(authorizationModelId); } - return call(() -> api.write(storeId, request)).thenApply(ClientWriteResponse::new); + return call(() -> api.write(storeId, body)).thenApply(ClientWriteResponse::new); } /** @@ -358,20 +367,21 @@ public class OpenFgaClient { * * @throws FgaInvalidParameterException When the Store ID is null, empty, or whitespace */ - public CompletableFuture deleteTuples(List tupleKeys) + public CompletableFuture deleteTuples(List tupleKeys) throws FgaInvalidParameterException { configuration.assertValid(); String storeId = configuration.getStoreIdChecked(); - var request = new WriteRequest(); - ClientTupleKey.asTupleKeys(tupleKeys).ifPresent(request::deletes); + var body = new WriteRequest(); + + body.deletes(ClientTupleKeyWithoutCondition.asWriteRequestDeletes(tupleKeys)); String authorizationModelId = configuration.getAuthorizationModelId(); if (!isNullOrWhitespace(authorizationModelId)) { - request.authorizationModelId(authorizationModelId); + body.authorizationModelId(authorizationModelId); } - return call(() -> api.write(storeId, request)).thenApply(ClientWriteResponse::new); + return call(() -> api.write(storeId, body)).thenApply(ClientWriteResponse::new); } /* ********************** @@ -401,10 +411,7 @@ public class OpenFgaClient { CheckRequest body = new CheckRequest(); if (request != null) { - body.tupleKey(new TupleKey() - .user(request.getUser()) - .relation(request.getRelation()) - ._object(request.getObject())); + body.tupleKey(request.asCheckRequestTupleKey()); var contextualTuples = request.getContextualTuples(); if (contextualTuples != null && !contextualTuples.isEmpty()) { @@ -480,9 +487,8 @@ public class OpenFgaClient { ExpandRequest body = new ExpandRequest(); if (request != null) { - body.tupleKey(new TupleKey() - .relation(request.getRelation()) - ._object(request.getObject())); + body.tupleKey( + new ExpandRequestTupleKey().relation(request.getRelation())._object(request.getObject())); } if (options != null && !isNullOrWhitespace(options.getAuthorizationModelId())) { @@ -518,10 +524,12 @@ public class OpenFgaClient { ListObjectsRequest body = new ListObjectsRequest(); if (request != null) { - body.user(request.getUser()) - .relation(request.getRelation()) - .type(request.getType()) - .contextualTuples(ClientTupleKey.asContextualTupleKeys(request.getContextualTupleKeys())); + body.user(request.getUser()).relation(request.getRelation()).type(request.getType()); + if (request.getContextualTupleKeys() != null) { + var contextualTuples = request.getContextualTupleKeys(); + var bodyContextualTuples = ClientTupleKey.asContextualTupleKeys(contextualTuples); + body.contextualTuples(bodyContextualTuples); + } } if (options != null && !isNullOrWhitespace(options.getAuthorizationModelId())) { diff --git a/config/clients/java/template/client-OpenFgaClientIntegrationTest.java.mustache b/config/clients/java/template/client-OpenFgaClientIntegrationTest.java.mustache index 4f38673e..b6d3c6b5 100644 --- a/config/clients/java/template/client-OpenFgaClientIntegrationTest.java.mustache +++ b/config/clients/java/template/client-OpenFgaClientIntegrationTest.java.mustache @@ -6,31 +6,51 @@ import static org.junit.jupiter.api.Assertions.*; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import {{invokerPackage}}.*; -import {{modelPackage}}.*; import {{configPackage}}.*; +import {{modelPackage}}.*; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; import dev.openfga.errors.ApiException; import java.net.http.HttpClient; import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestInstance.Lifecycle; +@TestInstance(Lifecycle.PER_CLASS) public class OpenFgaClientIntegrationTest { private static final ObjectMapper mapper = new ObjectMapper().findAndRegisterModules(); - private static final String DEFAULT_AUTH_MODEL = - "{\"schema_version\":\"1.1\",\"type_definitions\":[{\"type\":\"user\"},{\"type\":\"document\",\"relations\":{\"reader\":{\"this\":{}},\"writer\":{\"this\":{}},\"owner\":{\"this\":{}}},\"metadata\":{\"relations\":{\"reader\":{\"directly_related_user_types\":[{\"type\":\"user\"}]},\"writer\":{\"directly_related_user_types\":[{\"type\":\"user\"}]},\"owner\":{\"directly_related_user_types\":[{\"type\":\"user\"}]}}}}]}"; private static final String DEFAULT_USER = "user:81684243-9356-4421-8fbf-a4f8d36aa31b"; private static final String DEFAULT_DOC = "document:2021-budget"; - public static final ClientTupleKey DEFAULT_TUPLE_KEY = - new ClientTupleKey().user(DEFAULT_USER).relation("reader")._object(DEFAULT_DOC); - public static final List DEFAULT_TUPLE_KEYS = List.of(DEFAULT_TUPLE_KEY); - public static final ClientAssertion DEFAULT_ASSERTION = + private static final ClientTupleKeyWithoutCondition DEFAULT_TUPLE_KEY_NO_CONDITION = + new ClientTupleKeyWithoutCondition() + .user(DEFAULT_USER) + .relation("reader") + ._object(DEFAULT_DOC); + private static final ClientTupleKey DEFAULT_TUPLE_KEY = new ClientTupleKeyWithoutCondition() + .user(DEFAULT_USER) + .relation("reader") + ._object(DEFAULT_DOC) + .condition(null); // TODO: Add integ tests with conditions + private static final ClientAssertion DEFAULT_ASSERTION = new ClientAssertion().user(DEFAULT_USER).relation("reader")._object(DEFAULT_DOC).expectation(true); + private String authModelJson; private OpenFgaClient fga; + @BeforeAll + public void loadAuthModelJson() throws IOException { + authModelJson = Files.readString(Paths.get("src", "test-integration", "resources", "auth-model.json")); + } + @BeforeEach public void initializeApi() throws Exception { + System.setProperty("HttpRequestAttempt.debug-logging", "enable"); + ClientConfiguration apiConfig = new ClientConfiguration().apiUrl("http://localhost:8080"); fga = new OpenFgaClient(apiConfig); } @@ -121,7 +141,7 @@ public class OpenFgaClientIntegrationTest { assertEquals(authModelId, response.getAuthorizationModel().getId()); String typeDefsJson = mapper.writeValueAsString(authModel.getTypeDefinitions()); assertEquals( - "[{\"type\":\"user\",\"relations\":{},\"metadata\":null},{\"type\":\"document\",\"relations\":{\"owner\":{\"this\":{},\"computedUserset\":null,\"tupleToUserset\":null,\"union\":null,\"intersection\":null,\"difference\":null},\"reader\":{\"this\":{},\"computedUserset\":null,\"tupleToUserset\":null,\"union\":null,\"intersection\":null,\"difference\":null},\"writer\":{\"this\":{},\"computedUserset\":null,\"tupleToUserset\":null,\"union\":null,\"intersection\":null,\"difference\":null}},\"metadata\":{\"relations\":{\"owner\":{\"directly_related_user_types\":[{\"type\":\"user\",\"relation\":null,\"wildcard\":null}]},\"reader\":{\"directly_related_user_types\":[{\"type\":\"user\",\"relation\":null,\"wildcard\":null}]},\"writer\":{\"directly_related_user_types\":[{\"type\":\"user\",\"relation\":null,\"wildcard\":null}]}}}}]", + "[{\"type\":\"user\",\"relations\":{},\"metadata\":null},{\"type\":\"document\",\"relations\":{\"owner\":{\"this\":{},\"computedUserset\":null,\"tupleToUserset\":null,\"union\":null,\"intersection\":null,\"difference\":null},\"reader\":{\"this\":{},\"computedUserset\":null,\"tupleToUserset\":null,\"union\":null,\"intersection\":null,\"difference\":null},\"writer\":{\"this\":{},\"computedUserset\":null,\"tupleToUserset\":null,\"union\":null,\"intersection\":null,\"difference\":null}},\"metadata\":{\"relations\":{\"conditional_reader\":{\"directly_related_user_types\":[{\"type\":\"user\",\"relation\":null,\"wildcard\":null,\"condition\":\"name_starts_with_a\"}]},\"owner\":{\"directly_related_user_types\":[{\"type\":\"user\",\"relation\":null,\"wildcard\":null,\"condition\":\"\"}]},\"reader\":{\"directly_related_user_types\":[{\"type\":\"user\",\"relation\":null,\"wildcard\":null,\"condition\":\"\"}]},\"writer\":{\"directly_related_user_types\":[{\"type\":\"user\",\"relation\":null,\"wildcard\":null,\"condition\":\"\"}]}}}}]", typeDefsJson); } @@ -149,7 +169,7 @@ public class OpenFgaClientIntegrationTest { String typeDefsJson = mapper.writeValueAsString(authModel.getTypeDefinitions()); assertEquals( - "[{\"type\":\"user\",\"relations\":{},\"metadata\":null},{\"type\":\"document\",\"relations\":{\"owner\":{\"this\":{},\"computedUserset\":null,\"tupleToUserset\":null,\"union\":null,\"intersection\":null,\"difference\":null},\"reader\":{\"this\":{},\"computedUserset\":null,\"tupleToUserset\":null,\"union\":null,\"intersection\":null,\"difference\":null},\"writer\":{\"this\":{},\"computedUserset\":null,\"tupleToUserset\":null,\"union\":null,\"intersection\":null,\"difference\":null}},\"metadata\":{\"relations\":{\"owner\":{\"directly_related_user_types\":[{\"type\":\"user\",\"relation\":null,\"wildcard\":null}]},\"reader\":{\"directly_related_user_types\":[{\"type\":\"user\",\"relation\":null,\"wildcard\":null}]},\"writer\":{\"directly_related_user_types\":[{\"type\":\"user\",\"relation\":null,\"wildcard\":null}]}}}}]", + "[{\"type\":\"user\",\"relations\":{},\"metadata\":null},{\"type\":\"document\",\"relations\":{\"owner\":{\"this\":{},\"computedUserset\":null,\"tupleToUserset\":null,\"union\":null,\"intersection\":null,\"difference\":null},\"reader\":{\"this\":{},\"computedUserset\":null,\"tupleToUserset\":null,\"union\":null,\"intersection\":null,\"difference\":null},\"writer\":{\"this\":{},\"computedUserset\":null,\"tupleToUserset\":null,\"union\":null,\"intersection\":null,\"difference\":null}},\"metadata\":{\"relations\":{\"conditional_reader\":{\"directly_related_user_types\":[{\"type\":\"user\",\"relation\":null,\"wildcard\":null,\"condition\":\"name_starts_with_a\"}]},\"owner\":{\"directly_related_user_types\":[{\"type\":\"user\",\"relation\":null,\"wildcard\":null,\"condition\":\"\"}]},\"reader\":{\"directly_related_user_types\":[{\"type\":\"user\",\"relation\":null,\"wildcard\":null,\"condition\":\"\"}]},\"writer\":{\"directly_related_user_types\":[{\"type\":\"user\",\"relation\":null,\"wildcard\":null,\"condition\":\"\"}]}}}}]", typeDefsJson); } catch (JsonProcessingException ex) { assertNull(ex); @@ -163,8 +183,7 @@ public class OpenFgaClientIntegrationTest { String storeName = thisTestName(); String storeId = createStore(storeName); fga.setStoreId(storeId); - WriteAuthorizationModelRequest request = - mapper.readValue(DEFAULT_AUTH_MODEL, WriteAuthorizationModelRequest.class); + WriteAuthorizationModelRequest request = mapper.readValue(authModelJson, WriteAuthorizationModelRequest.class); // When WriteAuthorizationModelResponse response = fga.writeAuthorizationModel(request).get(); @@ -185,7 +204,8 @@ public class OpenFgaClientIntegrationTest { fga.setAuthorizationModelId(authModelId); ClientWriteRequest writeRequest = new ClientWriteRequest().writes(List.of(DEFAULT_TUPLE_KEY)); - ClientReadRequest readRequest = new ClientReadRequest().user(DEFAULT_USER)._object(DEFAULT_DOC); + ClientReadRequest readRequest = + new ClientReadRequest().user(DEFAULT_USER)._object(DEFAULT_DOC); // When fga.write(writeRequest).get(); @@ -291,7 +311,7 @@ public class OpenFgaClientIntegrationTest { // Then String responseJson = mapper.writeValueAsString(response.getAssertions()); assertEquals( - "[{\"tuple_key\":{\"object\":\"document:2021-budget\",\"relation\":\"reader\",\"user\":\"user:81684243-9356-4421-8fbf-a4f8d36aa31b\"},\"expectation\":true}]", + "[{\"tuple_key\":{\"user\":\"user:81684243-9356-4421-8fbf-a4f8d36aa31b\",\"relation\":\"reader\",\"object\":\"document:2021-budget\"},\"expectation\":true}]", responseJson); } @@ -313,10 +333,8 @@ public class OpenFgaClientIntegrationTest { */ private String writeAuthModel(String storeId) throws Exception { fga.setStoreId(storeId); - WriteAuthorizationModelRequest request = - mapper.readValue(DEFAULT_AUTH_MODEL, WriteAuthorizationModelRequest.class); - WriteAuthorizationModelResponse response = - fga.writeAuthorizationModel(request).get(); + var request = mapper.readValue(authModelJson, WriteAuthorizationModelRequest.class); + var response = fga.writeAuthorizationModel(request).get(); return response.getAuthorizationModelId(); } diff --git a/config/clients/java/template/client-OpenFgaClientTest.java.mustache b/config/clients/java/template/client-OpenFgaClientTest.java.mustache index e4ab6614..adbe913f 100644 --- a/config/clients/java/template/client-OpenFgaClientTest.java.mustache +++ b/config/clients/java/template/client-OpenFgaClientTest.java.mustache @@ -15,6 +15,7 @@ import {{errorsPackage}}.*; import java.net.http.HttpClient; import java.time.Duration; import java.util.List; +import java.util.Map; import java.util.concurrent.ExecutionException; import java.util.function.Function; import java.util.stream.Collectors; @@ -36,6 +37,8 @@ public class OpenFgaClientTest { private static final String DEFAULT_OBJECT = "document:budget"; private static final String DEFAULT_SCHEMA_VERSION = "1.1"; private static final String EMPTY_RESPONSE_BODY = "{}"; + private static final ClientRelationshipCondition DEFAULT_CONDITION = + new ClientRelationshipCondition().name("condition").context(Map.of("some", "context")); private static final int DEFAULT_MAX_RETRIES = 3; private static final Duration DEFAULT_RETRY_DELAY = Duration.ofMillis(100); @@ -46,8 +49,10 @@ public class OpenFgaClientTest { @BeforeEach public void beforeEachTest() throws Exception { + System.setProperty("HttpRequestAttempt.debug-logging", "enable"); + mockHttpClient = new HttpClientMock(); - // mockHttpClient.debugOn(); // Uncomment when debugging HTTP requests. + mockHttpClient.debugOn(); var mockHttpClientBuilder = mock(HttpClient.Builder.class); when(mockHttpClientBuilder.executor(any())).thenReturn(mockHttpClientBuilder); @@ -658,7 +663,7 @@ public class OpenFgaClientTest { // Given String postUrl = String.format("https://localhost/stores/%s/authorization-models", DEFAULT_STORE_ID); String expectedBody = - "{\"type_definitions\":[{\"type\":\"document\",\"relations\":{},\"metadata\":null}],\"schema_version\":\"1.1\"}"; + "{\"type_definitions\":[{\"type\":\"document\",\"relations\":{},\"metadata\":null}],\"schema_version\":\"1.1\",\"conditions\":{}}"; String responseBody = String.format("{\"authorization_model_id\":\"%s\"}", DEFAULT_AUTH_MODEL_ID); mockHttpClient.onPost(postUrl).withBody(is(expectedBody)).doReturn(201, responseBody); WriteAuthorizationModelRequest request = new WriteAuthorizationModelRequest() @@ -915,8 +920,8 @@ public class OpenFgaClientTest { // Given String postUrl = String.format("https://localhost/stores/%s/read", DEFAULT_STORE_ID); String expectedBody = String.format( - "{\"tuple_key\":{\"object\":\"%s\",\"relation\":\"%s\",\"user\":\"%s\"},\"page_size\":null,\"continuation_token\":null}", - DEFAULT_OBJECT, DEFAULT_RELATION, DEFAULT_USER); + "{\"tuple_key\":{\"user\":\"%s\",\"relation\":\"%s\",\"object\":\"%s\"},\"page_size\":null,\"continuation_token\":null}", + DEFAULT_USER, DEFAULT_RELATION, DEFAULT_OBJECT); String responseBody = String.format( "{\"tuples\":[{\"key\":{\"user\":\"%s\",\"relation\":\"%s\",\"object\":\"%s\"}}]}", DEFAULT_USER, DEFAULT_RELATION, DEFAULT_OBJECT); @@ -1041,8 +1046,8 @@ public class OpenFgaClientTest { // Given String postPath = "https://localhost/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write"; String expectedBody = String.format( - "{\"writes\":{\"tuple_keys\":[{\"object\":\"%s\",\"relation\":\"%s\",\"user\":\"%s\"}]},\"deletes\":null,\"authorization_model_id\":\"%s\"}", - DEFAULT_OBJECT, DEFAULT_RELATION, DEFAULT_USER, DEFAULT_AUTH_MODEL_ID); + "{\"writes\":{\"tuple_keys\":[{\"user\":\"%s\",\"relation\":\"%s\",\"object\":\"%s\",\"condition\":null}]},\"deletes\":null,\"authorization_model_id\":\"%s\"}", + DEFAULT_USER, DEFAULT_RELATION, DEFAULT_OBJECT, DEFAULT_AUTH_MODEL_ID); mockHttpClient.onPost(postPath).withBody(is(expectedBody)).doReturn(200, EMPTY_RESPONSE_BODY); ClientWriteRequest request = new ClientWriteRequest() .writes(List.of(new ClientTupleKey() @@ -1066,11 +1071,11 @@ public class OpenFgaClientTest { // Given String postPath = "https://localhost/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write"; String expectedBody = String.format( - "{\"writes\":null,\"deletes\":{\"tuple_keys\":[{\"object\":\"%s\",\"relation\":\"%s\",\"user\":\"%s\"}]},\"authorization_model_id\":\"%s\"}", - DEFAULT_OBJECT, DEFAULT_RELATION, DEFAULT_USER, DEFAULT_AUTH_MODEL_ID); + "{\"writes\":null,\"deletes\":{\"tuple_keys\":[{\"user\":\"%s\",\"relation\":\"%s\",\"object\":\"%s\"}]},\"authorization_model_id\":\"%s\"}", + DEFAULT_USER, DEFAULT_RELATION, DEFAULT_OBJECT, DEFAULT_AUTH_MODEL_ID); mockHttpClient.onPost(postPath).withBody(is(expectedBody)).doReturn(200, EMPTY_RESPONSE_BODY); ClientWriteRequest request = new ClientWriteRequest() - .deletes(List.of(new ClientTupleKey() + .deletes(List.of(new ClientTupleKeyWithoutCondition() ._object(DEFAULT_OBJECT) .relation(DEFAULT_RELATION) .user(DEFAULT_USER))); @@ -1086,31 +1091,35 @@ public class OpenFgaClientTest { public void writeTest_transactions() throws Exception { // Given String postPath = "https://localhost/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write"; - String tupleBody = String.format( - "{\"object\":\"%s\",\"relation\":\"%s\",\"user\":\"%s\"}", - DEFAULT_OBJECT, DEFAULT_RELATION, DEFAULT_USER); - ClientTupleKey tuple = new ClientTupleKey() + String writeTupleBody = String.format( + "{\"user\":\"%s\",\"relation\":\"%s\",\"object\":\"%s\",\"condition\":{\"name\":\"condition\",\"context\":{\"some\":\"context\"}}}", + DEFAULT_USER, DEFAULT_RELATION, DEFAULT_OBJECT); + ClientTupleKeyWithoutCondition tuple = new ClientTupleKeyWithoutCondition() ._object(DEFAULT_OBJECT) .relation(DEFAULT_RELATION) .user(DEFAULT_USER); + ClientTupleKey writeTuple = tuple.condition(DEFAULT_CONDITION); String write2Body = String.format( "{\"writes\":{\"tuple_keys\":[%s,%s]},\"deletes\":null,\"authorization_model_id\":\"%s\"}", - tupleBody, tupleBody, DEFAULT_AUTH_MODEL_ID); + writeTupleBody, writeTupleBody, DEFAULT_AUTH_MODEL_ID); String write1Body = String.format( "{\"writes\":{\"tuple_keys\":[%s]},\"deletes\":null,\"authorization_model_id\":\"%s\"}", - tupleBody, DEFAULT_AUTH_MODEL_ID); + writeTupleBody, DEFAULT_AUTH_MODEL_ID); + String deleteTupleBody = String.format( + "{\"user\":\"%s\",\"relation\":\"%s\",\"object\":\"%s\"}", + DEFAULT_USER, DEFAULT_RELATION, DEFAULT_OBJECT); String delete2Body = String.format( "{\"writes\":null,\"deletes\":{\"tuple_keys\":[%s,%s]},\"authorization_model_id\":\"%s\"}", - tupleBody, tupleBody, DEFAULT_AUTH_MODEL_ID); + deleteTupleBody, deleteTupleBody, DEFAULT_AUTH_MODEL_ID); String delete1Body = String.format( "{\"writes\":null,\"deletes\":{\"tuple_keys\":[%s]},\"authorization_model_id\":\"%s\"}", - tupleBody, DEFAULT_AUTH_MODEL_ID); + deleteTupleBody, DEFAULT_AUTH_MODEL_ID); mockHttpClient .onPost(postPath) .withBody(isOneOf(write2Body, write1Body, delete2Body, delete1Body)) .doReturn(200, EMPTY_RESPONSE_BODY); ClientWriteRequest request = new ClientWriteRequest() - .writes(List.of(tuple, tuple, tuple, tuple, tuple)) + .writes(List.of(writeTuple, writeTuple, writeTuple, writeTuple, writeTuple)) .deletes(List.of(tuple, tuple, tuple, tuple, tuple)); ClientWriteOptions options = new ClientWriteOptions().disableTransactions(false).transactionChunkSize(2); @@ -1134,8 +1143,8 @@ public class OpenFgaClientTest { String failedUser = "user:SECOND"; String skippedUser = "user:third"; Function writeBody = user -> String.format( - "{\"writes\":{\"tuple_keys\":[{\"object\":\"%s\",\"relation\":\"%s\",\"user\":\"%s\"}]},\"deletes\":null,\"authorization_model_id\":\"%s\"}", - DEFAULT_OBJECT, DEFAULT_RELATION, user, DEFAULT_AUTH_MODEL_ID); + "{\"writes\":{\"tuple_keys\":[{\"user\":\"%s\",\"relation\":\"%s\",\"object\":\"%s\",\"condition\":{\"name\":\"condition\",\"context\":{\"some\":\"context\"}}}]},\"deletes\":null,\"authorization_model_id\":\"%s\"}", + user, DEFAULT_RELATION, DEFAULT_OBJECT, DEFAULT_AUTH_MODEL_ID); mockHttpClient .onPost(postPath) .withBody(isOneOf(writeBody.apply(firstUser), writeBody.apply(skippedUser))) @@ -1149,7 +1158,8 @@ public class OpenFgaClientTest { .map(user -> new ClientTupleKey() ._object(DEFAULT_OBJECT) .relation(DEFAULT_RELATION) - .user(user)) + .user(user) + .condition(DEFAULT_CONDITION)) .collect(Collectors.toList())); ClientWriteOptions options = new ClientWriteOptions().disableTransactions(false).transactionChunkSize(1); @@ -1182,20 +1192,27 @@ public class OpenFgaClientTest { public void writeTest_nonTransaction() throws Exception { // Given String postPath = "https://localhost/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write"; - String tupleBody = String.format( - "{\"object\":\"%s\",\"relation\":\"%s\",\"user\":\"%s\"}", - DEFAULT_OBJECT, DEFAULT_RELATION, DEFAULT_USER); + String writeTupleBody = String.format( + "{\"user\":\"%s\",\"relation\":\"%s\",\"object\":\"%s\",\"condition\":{\"name\":\"condition\",\"context\":{\"some\":\"context\"}}}", + DEFAULT_USER, DEFAULT_RELATION, DEFAULT_OBJECT); + String deleteTupleBody = String.format( + "{\"user\":\"%s\",\"relation\":\"%s\",\"object\":\"%s\"}", + DEFAULT_USER, DEFAULT_RELATION, DEFAULT_OBJECT); String expectedBody = String.format( "{\"writes\":{\"tuple_keys\":[%s,%s,%s]},\"deletes\":{\"tuple_keys\":[%s,%s,%s]},\"authorization_model_id\":\"%s\"}", - tupleBody, tupleBody, tupleBody, tupleBody, tupleBody, tupleBody, DEFAULT_AUTH_MODEL_ID); - mockHttpClient - .onPost(postPath) - .withBody(is(expectedBody)) - .doReturn(200, EMPTY_RESPONSE_BODY); - ClientTupleKey tuple = new ClientTupleKey() + writeTupleBody, + writeTupleBody, + writeTupleBody, + deleteTupleBody, + deleteTupleBody, + deleteTupleBody, + DEFAULT_AUTH_MODEL_ID); + mockHttpClient.onPost(postPath).withBody(is(expectedBody)).doReturn(200, EMPTY_RESPONSE_BODY); + var tuple = new ClientTupleKey() ._object(DEFAULT_OBJECT) .relation(DEFAULT_RELATION) - .user(DEFAULT_USER); + .user(DEFAULT_USER) + .condition(DEFAULT_CONDITION); ClientWriteRequest request = new ClientWriteRequest() .writes(List.of(tuple, tuple, tuple)) .deletes(List.of(tuple, tuple, tuple)); @@ -1216,20 +1233,30 @@ public class OpenFgaClientTest { public void writeTest_nonTransactionsWithFailure() throws Exception { // Given String postPath = "https://localhost/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write"; - String tupleBody = String.format( - "{\"object\":\"%s\",\"relation\":\"%s\",\"user\":\"%s\"}", - DEFAULT_OBJECT, DEFAULT_RELATION, DEFAULT_USER); + String writeTupleBody = String.format( + "{\"user\":\"%s\",\"relation\":\"%s\",\"object\":\"%s\",\"condition\":{\"name\":\"condition\",\"context\":{\"some\":\"context\"}}}", + DEFAULT_USER, DEFAULT_RELATION, DEFAULT_OBJECT); + String deleteTupleBody = String.format( + "{\"user\":\"%s\",\"relation\":\"%s\",\"object\":\"%s\"}", + DEFAULT_USER, DEFAULT_RELATION, DEFAULT_OBJECT); String expectedBody = String.format( "{\"writes\":{\"tuple_keys\":[%s,%s,%s]},\"deletes\":{\"tuple_keys\":[%s,%s,%s]},\"authorization_model_id\":\"%s\"}", - tupleBody, tupleBody, tupleBody, tupleBody, tupleBody, tupleBody, DEFAULT_AUTH_MODEL_ID); + writeTupleBody, + writeTupleBody, + writeTupleBody, + deleteTupleBody, + deleteTupleBody, + deleteTupleBody, + DEFAULT_AUTH_MODEL_ID); mockHttpClient .onPost(postPath) .withBody(is(expectedBody)) .doReturn(400, "{\"code\":\"validation_error\",\"message\":\"Generic validation error\"}"); - ClientTupleKey tuple = new ClientTupleKey() + var tuple = new ClientTupleKey() ._object(DEFAULT_OBJECT) .relation(DEFAULT_RELATION) - .user(DEFAULT_USER); + .user(DEFAULT_USER) + .condition(DEFAULT_CONDITION); ClientWriteRequest request = new ClientWriteRequest() .writes(List.of(tuple, tuple, tuple)) .deletes(List.of(tuple, tuple, tuple)); @@ -1252,13 +1279,15 @@ public class OpenFgaClientTest { // Given String postPath = String.format("https://localhost/stores/%s/write", DEFAULT_STORE_ID); String expectedBody = String.format( - "{\"writes\":{\"tuple_keys\":[{\"object\":\"%s\",\"relation\":\"%s\",\"user\":\"%s\"}]},\"deletes\":null,\"authorization_model_id\":\"%s\"}", - DEFAULT_OBJECT, DEFAULT_RELATION, DEFAULT_USER, DEFAULT_AUTH_MODEL_ID); + "{\"writes\":{\"tuple_keys\":[{\"user\":\"%s\",\"relation\":\"%s\",\"object\":\"%s\",\"condition\":{\"name\":\"condition\",\"context\":{\"some\":\"context\"}}}]}," + + "\"deletes\":null,\"authorization_model_id\":\"%s\"}", + DEFAULT_USER, DEFAULT_RELATION, DEFAULT_OBJECT, DEFAULT_AUTH_MODEL_ID); mockHttpClient.onPost(postPath).withBody(is(expectedBody)).doReturn(200, EMPTY_RESPONSE_BODY); List tuples = List.of(new ClientTupleKey() ._object(DEFAULT_OBJECT) .relation(DEFAULT_RELATION) - .user(DEFAULT_USER)); + .user(DEFAULT_USER) + .condition(DEFAULT_CONDITION)); // When ClientWriteResponse response = fga.writeTuples(tuples).get(); @@ -1273,10 +1302,10 @@ public class OpenFgaClientTest { // Given String postPath = String.format("https://localhost/stores/%s/write", DEFAULT_STORE_ID); String expectedBody = String.format( - "{\"writes\":null,\"deletes\":{\"tuple_keys\":[{\"object\":\"%s\",\"relation\":\"%s\",\"user\":\"%s\"}]},\"authorization_model_id\":\"%s\"}", - DEFAULT_OBJECT, DEFAULT_RELATION, DEFAULT_USER, DEFAULT_AUTH_MODEL_ID); + "{\"writes\":null,\"deletes\":{\"tuple_keys\":[{\"user\":\"%s\",\"relation\":\"%s\",\"object\":\"%s\"}]},\"authorization_model_id\":\"%s\"}", + DEFAULT_USER, DEFAULT_RELATION, DEFAULT_OBJECT, DEFAULT_AUTH_MODEL_ID); mockHttpClient.onPost(postPath).withBody(is(expectedBody)).doReturn(200, EMPTY_RESPONSE_BODY); - List tuples = List.of(new ClientTupleKey() + List tuples = List.of(new ClientTupleKeyWithoutCondition() ._object(DEFAULT_OBJECT) .relation(DEFAULT_RELATION) .user(DEFAULT_USER)); @@ -1314,7 +1343,8 @@ public class OpenFgaClientTest { .writes(List.of(new ClientTupleKey() ._object(DEFAULT_OBJECT) .relation(DEFAULT_RELATION) - .user(DEFAULT_USER))); + .user(DEFAULT_USER) + .condition(DEFAULT_CONDITION))); // When ExecutionException execException = @@ -1340,7 +1370,8 @@ public class OpenFgaClientTest { .writes(List.of(new ClientTupleKey() ._object(DEFAULT_OBJECT) .relation(DEFAULT_RELATION) - .user(DEFAULT_USER))); + .user(DEFAULT_USER) + .condition(DEFAULT_CONDITION))); // When ExecutionException execException = @@ -1365,7 +1396,8 @@ public class OpenFgaClientTest { .writes(List.of(new ClientTupleKey() ._object(DEFAULT_OBJECT) .relation(DEFAULT_RELATION) - .user(DEFAULT_USER))); + .user(DEFAULT_USER) + .condition(DEFAULT_CONDITION))); // When ExecutionException execException = @@ -1387,21 +1419,20 @@ public class OpenFgaClientTest { // Given String postUrl = String.format("https://localhost/stores/%s/check", DEFAULT_STORE_ID); String expectedBody = String.format( - "{\"tuple_key\":{\"object\":\"%s\",\"relation\":\"%s\",\"user\":\"%s\"}," + - "\"contextual_tuples\":{\"tuple_keys\":[{\"object\":\"%s\",\"relation\":\"owner\",\"user\":\"%s\"}]}," + - "\"authorization_model_id\":\"01G5JAVJ41T49E9TT3SKVS7X1J\",\"trace\":null}", - DEFAULT_OBJECT, DEFAULT_RELATION, DEFAULT_USER, DEFAULT_OBJECT, DEFAULT_USER); + "{\"tuple_key\":{\"user\":\"%s\",\"relation\":\"%s\",\"object\":\"%s\"}," + + "\"contextual_tuples\":{\"tuple_keys\":[{\"user\":\"%s\",\"relation\":\"owner\",\"object\":\"%s\",\"condition\":{\"name\":\"condition\",\"context\":{\"some\":\"context\"}}}]}," + + "\"authorization_model_id\":\"01G5JAVJ41T49E9TT3SKVS7X1J\",\"trace\":null,\"context\":null}", + DEFAULT_USER, DEFAULT_RELATION, DEFAULT_OBJECT, DEFAULT_USER, DEFAULT_OBJECT); mockHttpClient.onPost(postUrl).withBody(is(expectedBody)).doReturn(200, "{\"allowed\":true}"); ClientCheckRequest request = new ClientCheckRequest() ._object(DEFAULT_OBJECT) .relation(DEFAULT_RELATION) .user(DEFAULT_USER) - .contextualTuples(List.of( - new ClientTupleKey() - ._object(DEFAULT_OBJECT) - .relation("owner") - .user(DEFAULT_USER) - )); + .contextualTuples(List.of(new ClientTupleKey() + ._object(DEFAULT_OBJECT) + .relation("owner") + .user(DEFAULT_USER) + .condition(DEFAULT_CONDITION))); ClientCheckOptions options = new ClientCheckOptions().authorizationModelId(DEFAULT_AUTH_MODEL_ID); // When @@ -1498,8 +1529,8 @@ public class OpenFgaClientTest { // Given String postUrl = String.format("https://localhost/stores/%s/check", DEFAULT_STORE_ID); String expectedBody = String.format( - "{\"tuple_key\":{\"object\":\"%s\",\"relation\":\"%s\",\"user\":\"%s\"},\"contextual_tuples\":null,\"authorization_model_id\":\"01G5JAVJ41T49E9TT3SKVS7X1J\",\"trace\":null}", - DEFAULT_OBJECT, DEFAULT_RELATION, DEFAULT_USER); + "{\"tuple_key\":{\"user\":\"%s\",\"relation\":\"%s\",\"object\":\"%s\"},\"contextual_tuples\":null,\"authorization_model_id\":\"01G5JAVJ41T49E9TT3SKVS7X1J\",\"trace\":null,\"context\":null}", + DEFAULT_USER, DEFAULT_RELATION, DEFAULT_OBJECT); mockHttpClient.onPost(postUrl).withBody(is(expectedBody)).doReturn(200, "{\"allowed\":true}"); ClientCheckRequest request = new ClientCheckRequest() ._object(DEFAULT_OBJECT) @@ -1521,8 +1552,8 @@ public class OpenFgaClientTest { // Given String postUrl = String.format("https://localhost/stores/%s/check", DEFAULT_STORE_ID); String expectedBody = String.format( - "{\"tuple_key\":{\"object\":\"%s\",\"relation\":\"%s\",\"user\":\"%s\"},\"contextual_tuples\":null,\"authorization_model_id\":\"01G5JAVJ41T49E9TT3SKVS7X1J\",\"trace\":null}", - DEFAULT_OBJECT, DEFAULT_RELATION, DEFAULT_USER); + "{\"tuple_key\":{\"user\":\"%s\",\"relation\":\"%s\",\"object\":\"%s\"},\"contextual_tuples\":null,\"authorization_model_id\":\"01G5JAVJ41T49E9TT3SKVS7X1J\",\"trace\":null,\"context\":null}", + DEFAULT_USER, DEFAULT_RELATION, DEFAULT_OBJECT); mockHttpClient.onPost(postUrl).withBody(is(expectedBody)).doReturn(200, "{\"allowed\":true}"); List requests = IntStream.range(0, 20) .mapToObj(ignored -> new ClientCheckRequest() @@ -1639,8 +1670,8 @@ public class OpenFgaClientTest { // Given String postPath = "https://localhost/stores/01YCP46JKYM8FJCQ37NMBYHE5X/expand"; String expectedBody = String.format( - "{\"tuple_key\":{\"object\":\"%s\",\"relation\":\"%s\",\"user\":null},\"authorization_model_id\":\"%s\"}", - DEFAULT_OBJECT, DEFAULT_RELATION, DEFAULT_AUTH_MODEL_ID); + "{\"tuple_key\":{\"relation\":\"%s\",\"object\":\"%s\"},\"authorization_model_id\":\"%s\"}", + DEFAULT_RELATION, DEFAULT_OBJECT, DEFAULT_AUTH_MODEL_ID); String responseBody = String.format( "{\"tree\":{\"root\":{\"union\":{\"nodes\":[{\"leaf\":{\"users\":{\"users\":[\"%s\"]}}}]}}}}", DEFAULT_USER); @@ -1755,7 +1786,7 @@ public class OpenFgaClientTest { // Given String postPath = String.format("https://localhost/stores/%s/list-objects", DEFAULT_STORE_ID); String expectedBody = String.format( - "{\"authorization_model_id\":\"%s\",\"type\":null,\"relation\":\"%s\",\"user\":\"%s\",\"contextual_tuples\":{\"tuple_keys\":[]}}", + "{\"authorization_model_id\":\"%s\",\"type\":null,\"relation\":\"%s\",\"user\":\"%s\",\"contextual_tuples\":null,\"context\":null}", DEFAULT_AUTH_MODEL_ID, DEFAULT_RELATION, DEFAULT_USER); mockHttpClient .onPost(postPath) @@ -1859,8 +1890,8 @@ public class OpenFgaClientTest { // Given String postUrl = String.format("https://localhost/stores/%s/check", DEFAULT_STORE_ID); String expectedBody = String.format( - "{\"tuple_key\":{\"object\":\"%s\",\"relation\":\"%s\",\"user\":\"%s\"},\"contextual_tuples\":null,\"authorization_model_id\":\"01G5JAVJ41T49E9TT3SKVS7X1J\",\"trace\":null}", - DEFAULT_OBJECT, DEFAULT_RELATION, DEFAULT_USER); + "{\"tuple_key\":{\"user\":\"%s\",\"relation\":\"%s\",\"object\":\"%s\"},\"contextual_tuples\":null,\"authorization_model_id\":\"01G5JAVJ41T49E9TT3SKVS7X1J\",\"trace\":null,\"context\":null}", + DEFAULT_USER, DEFAULT_RELATION, DEFAULT_OBJECT); mockHttpClient.onPost(postUrl).withBody(is(expectedBody)).doReturn(200, "{\"allowed\":true}"); ClientListRelationsRequest request = new ClientListRelationsRequest() .relations(List.of(DEFAULT_RELATION)) @@ -1886,8 +1917,8 @@ public class OpenFgaClientTest { // Given String postUrl = String.format("https://localhost/stores/%s/check", DEFAULT_STORE_ID); String expectedBody = String.format( - "{\"tuple_key\":{\"object\":\"%s\",\"relation\":\"%s\",\"user\":\"%s\"},\"contextual_tuples\":null,\"authorization_model_id\":\"01G5JAVJ41T49E9TT3SKVS7X1J\",\"trace\":null}", - DEFAULT_OBJECT, "owner", DEFAULT_USER); + "{\"tuple_key\":{\"user\":\"%s\",\"relation\":\"%s\",\"object\":\"%s\"},\"contextual_tuples\":null,\"authorization_model_id\":\"%s\",\"trace\":null,\"context\":null}", + DEFAULT_USER, "owner", DEFAULT_OBJECT, DEFAULT_AUTH_MODEL_ID); mockHttpClient.onPost(postUrl).withBody(is(expectedBody)).doReturn(200, "{\"allowed\":false}"); ClientListRelationsRequest request = new ClientListRelationsRequest() .relations(List.of("owner")) @@ -1967,8 +1998,8 @@ public class OpenFgaClientTest { // Given String postUrl = String.format("https://localhost/stores/%s/check", DEFAULT_STORE_ID); String expectedBody = String.format( - "{\"tuple_key\":{\"object\":\"%s\",\"relation\":\"%s\",\"user\":\"%s\"},\"contextual_tuples\":null,\"authorization_model_id\":\"01G5JAVJ41T49E9TT3SKVS7X1J\",\"trace\":null}", - DEFAULT_OBJECT, DEFAULT_RELATION, DEFAULT_USER); + "{\"tuple_key\":{\"user\":\"%s\",\"relation\":\"%s\",\"object\":\"%s\"},\"contextual_tuples\":null,\"authorization_model_id\":\"01G5JAVJ41T49E9TT3SKVS7X1J\",\"trace\":null,\"context\":null}", + DEFAULT_USER, DEFAULT_RELATION, DEFAULT_OBJECT); mockHttpClient .onPost(postUrl) .withBody(is(expectedBody)) @@ -1997,8 +2028,8 @@ public class OpenFgaClientTest { // Given String postUrl = String.format("https://localhost/stores/%s/check", DEFAULT_STORE_ID); String expectedBody = String.format( - "{\"tuple_key\":{\"object\":\"%s\",\"relation\":\"%s\",\"user\":\"%s\"},\"contextual_tuples\":null,\"authorization_model_id\":\"01G5JAVJ41T49E9TT3SKVS7X1J\",\"trace\":null}", - DEFAULT_OBJECT, DEFAULT_RELATION, DEFAULT_USER); + "{\"tuple_key\":{\"user\":\"%s\",\"relation\":\"%s\",\"object\":\"%s\"},\"contextual_tuples\":null,\"authorization_model_id\":\"01G5JAVJ41T49E9TT3SKVS7X1J\",\"trace\":null,\"context\":null}", + DEFAULT_USER, DEFAULT_RELATION, DEFAULT_OBJECT); mockHttpClient .onPost(postUrl) .withBody(is(expectedBody)) @@ -2026,8 +2057,8 @@ public class OpenFgaClientTest { // Given String postUrl = String.format("https://localhost/stores/%s/check", DEFAULT_STORE_ID); String expectedBody = String.format( - "{\"tuple_key\":{\"object\":\"%s\",\"relation\":\"%s\",\"user\":\"%s\"},\"contextual_tuples\":null,\"authorization_model_id\":\"01G5JAVJ41T49E9TT3SKVS7X1J\",\"trace\":null}", - DEFAULT_OBJECT, DEFAULT_RELATION, DEFAULT_USER); + "{\"tuple_key\":{\"user\":\"%s\",\"relation\":\"%s\",\"object\":\"%s\"},\"contextual_tuples\":null,\"authorization_model_id\":\"01G5JAVJ41T49E9TT3SKVS7X1J\",\"trace\":null,\"context\":null}", + DEFAULT_USER, DEFAULT_RELATION, DEFAULT_OBJECT); mockHttpClient .onPost(postUrl) .withBody(is(expectedBody)) @@ -2059,8 +2090,8 @@ public class OpenFgaClientTest { String getUrl = String.format("https://localhost/stores/%s/assertions/%s", DEFAULT_STORE_ID, DEFAULT_AUTH_MODEL_ID); String responseBody = String.format( - "{\"assertions\":[{\"tuple_key\":{\"object\":\"%s\",\"relation\":\"%s\",\"user\":\"%s\"},\"expectation\":true}]}", - DEFAULT_OBJECT, DEFAULT_RELATION, DEFAULT_USER); + "{\"assertions\":[{\"tuple_key\":{\"user\":\"%s\",\"relation\":\"%s\",\"object\":\"%s\"},\"expectation\":true}]}", + DEFAULT_USER, DEFAULT_RELATION, DEFAULT_OBJECT); mockHttpClient.onGet(getUrl).doReturn(200, responseBody); // When @@ -2180,8 +2211,8 @@ public class OpenFgaClientTest { String putUrl = String.format("https://localhost/stores/%s/assertions/%s", DEFAULT_STORE_ID, DEFAULT_AUTH_MODEL_ID); String expectedBody = String.format( - "{\"assertions\":[{\"tuple_key\":{\"object\":\"%s\",\"relation\":\"%s\",\"user\":\"%s\"},\"expectation\":true}]}", - DEFAULT_OBJECT, DEFAULT_RELATION, DEFAULT_USER); + "{\"assertions\":[{\"tuple_key\":{\"user\":\"%s\",\"relation\":\"%s\",\"object\":\"%s\"},\"expectation\":true}]}", + DEFAULT_USER, DEFAULT_RELATION, DEFAULT_OBJECT); mockHttpClient.onPut(putUrl).withBody(is(expectedBody)).doReturn(200, EMPTY_RESPONSE_BODY); List assertions = List.of(new ClientAssertion() .user(DEFAULT_USER) diff --git a/config/clients/java/template/creds-OAuth2ClientTest.java.mustache b/config/clients/java/template/creds-OAuth2ClientTest.java.mustache index 898f35fb..7f4706be 100644 --- a/config/clients/java/template/creds-OAuth2ClientTest.java.mustache +++ b/config/clients/java/template/creds-OAuth2ClientTest.java.mustache @@ -29,7 +29,10 @@ class OAuth2ClientTest { @BeforeEach public void setup() throws FgaInvalidParameterException { + System.setProperty("HttpRequestAttempt.debug-logging", "enable"); + mockHttpClient = new HttpClientMock(); + mockHttpClient.debugOn(); var credentials = new Credentials(new ClientCredentials() .clientId(CLIENT_ID)