From 6ab70795364de5864ce1a4d936dbdadc132d5267 Mon Sep 17 00:00:00 2001 From: "J.R. Hill" Date: Tue, 5 Dec 2023 17:33:52 -0800 Subject: [PATCH] feat: add ABAC support to OpenFgaClient --- .github/dependabot.yaml | 18 +++++ .openapi-generator/FILES | 3 + docs/AuthorizationModel.md | 2 +- docs/RelationshipCondition.md | 2 +- .../sdk/api/client/ClientCheckRequest.java | 11 ++- .../api/client/ClientListObjectsRequest.java | 6 +- .../client/ClientRelationshipCondition.java | 42 ++++++++++ .../sdk/api/client/ClientTupleKey.java | 30 ++++++++ .../client/ClientTupleKeyWithCondition.java | 76 +++++++++++++++++++ .../sdk/api/client/ClientWriteRequest.java | 8 +- .../openfga/sdk/api/client/OpenFgaClient.java | 57 +++----------- .../sdk/api/model/AuthorizationModel.java | 6 +- .../dev/openfga/sdk/api/model/NullValue.java | 2 +- .../sdk/api/model/RelationshipCondition.java | 6 +- .../client/OpenFgaClientIntegrationTest.java | 20 +++-- .../sdk/api/client/OpenFgaClientTest.java | 61 +++++++++------ 16 files changed, 253 insertions(+), 97 deletions(-) create mode 100644 .github/dependabot.yaml create mode 100644 src/main/java/dev/openfga/sdk/api/client/ClientRelationshipCondition.java create mode 100644 src/main/java/dev/openfga/sdk/api/client/ClientTupleKeyWithCondition.java diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml new file mode 100644 index 0000000..5a916f6 --- /dev/null +++ b/.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/.openapi-generator/FILES b/.openapi-generator/FILES index f672c77..72db90d 100644 --- a/.openapi-generator/FILES +++ b/.openapi-generator/FILES @@ -2,6 +2,7 @@ .github/CODEOWNERS .github/ISSUE_TEMPLATE/bug_report.md .github/ISSUE_TEMPLATE/feature_request.md +.github/dependabot.yaml .github/workflows/main.yml .github/workflows/semgrep.yaml .gitignore @@ -112,7 +113,9 @@ src/main/java/dev/openfga/sdk/api/client/ClientReadAuthorizationModelsResponse.j src/main/java/dev/openfga/sdk/api/client/ClientReadChangesResponse.java src/main/java/dev/openfga/sdk/api/client/ClientReadRequest.java src/main/java/dev/openfga/sdk/api/client/ClientReadResponse.java +src/main/java/dev/openfga/sdk/api/client/ClientRelationshipCondition.java src/main/java/dev/openfga/sdk/api/client/ClientTupleKey.java +src/main/java/dev/openfga/sdk/api/client/ClientTupleKeyWithCondition.java src/main/java/dev/openfga/sdk/api/client/ClientWriteAssertionsResponse.java src/main/java/dev/openfga/sdk/api/client/ClientWriteAuthorizationModelResponse.java src/main/java/dev/openfga/sdk/api/client/ClientWriteRequest.java diff --git a/docs/AuthorizationModel.md b/docs/AuthorizationModel.md index b93d330..5ef00db 100644 --- a/docs/AuthorizationModel.md +++ b/docs/AuthorizationModel.md @@ -7,7 +7,7 @@ | Name | Type | Description | Notes | |------------ | ------------- | ------------- | -------------| -|**id** | **String** | | [optional] | +|**id** | **String** | | | |**schemaVersion** | **String** | | | |**typeDefinitions** | [**List<TypeDefinition>**](TypeDefinition.md) | | | |**conditions** | [**Map<String, Condition>**](Condition.md) | | [optional] | diff --git a/docs/RelationshipCondition.md b/docs/RelationshipCondition.md index dc39e38..d10f303 100644 --- a/docs/RelationshipCondition.md +++ b/docs/RelationshipCondition.md @@ -8,7 +8,7 @@ | Name | Type | Description | Notes | |------------ | ------------- | ------------- | -------------| |**name** | **String** | A reference (by name) of the relationship condition defined in the authorization model. | | -|**context** | **Object** | Additional context/data to persist along with the condition. The keys must match the parameters defined by the condition, and the value types must match the parameter type definitions. | | +|**context** | **Object** | Additional context/data to persist along with the condition. The keys must match the parameters defined by the condition, and the value types must match the parameter type definitions. | [optional] | diff --git a/src/main/java/dev/openfga/sdk/api/client/ClientCheckRequest.java b/src/main/java/dev/openfga/sdk/api/client/ClientCheckRequest.java index 1ce257c..9ac6fda 100644 --- a/src/main/java/dev/openfga/sdk/api/client/ClientCheckRequest.java +++ b/src/main/java/dev/openfga/sdk/api/client/ClientCheckRequest.java @@ -12,13 +12,18 @@ package dev.openfga.sdk.api.client; +import dev.openfga.sdk.api.model.CheckRequestTupleKey; import java.util.List; public class ClientCheckRequest { private String user; private String relation; private String _object; - private List contextualTuples; + private List contextualTuples; + + public CheckRequestTupleKey asCheckRequestTupleKey() { + return new CheckRequestTupleKey().user(user).relation(relation)._object(_object); + } public ClientCheckRequest _object(String _object) { this._object = _object; @@ -59,12 +64,12 @@ public String getUser() { return user; } - public ClientCheckRequest contextualTuples(List contextualTuples) { + public ClientCheckRequest contextualTuples(List contextualTuples) { this.contextualTuples = contextualTuples; return this; } - public List getContextualTuples() { + public List getContextualTuples() { return contextualTuples; } } diff --git a/src/main/java/dev/openfga/sdk/api/client/ClientListObjectsRequest.java b/src/main/java/dev/openfga/sdk/api/client/ClientListObjectsRequest.java index 9f25b34..9a03bda 100644 --- a/src/main/java/dev/openfga/sdk/api/client/ClientListObjectsRequest.java +++ b/src/main/java/dev/openfga/sdk/api/client/ClientListObjectsRequest.java @@ -18,7 +18,7 @@ public class ClientListObjectsRequest { private String user; private String relation; private String type; - private List contextualTupleKeys; + private List contextualTupleKeys; public ClientListObjectsRequest user(String user) { this.user = user; @@ -55,12 +55,12 @@ public String getType() { return type; } - public ClientListObjectsRequest contextualTupleKeys(List contextualTupleKeys) { + public ClientListObjectsRequest contextualTupleKeys(List contextualTupleKeys) { this.contextualTupleKeys = contextualTupleKeys; return this; } - public List getContextualTupleKeys() { + public List getContextualTupleKeys() { return contextualTupleKeys; } } diff --git a/src/main/java/dev/openfga/sdk/api/client/ClientRelationshipCondition.java b/src/main/java/dev/openfga/sdk/api/client/ClientRelationshipCondition.java new file mode 100644 index 0000000..6af608e --- /dev/null +++ b/src/main/java/dev/openfga/sdk/api/client/ClientRelationshipCondition.java @@ -0,0 +1,42 @@ +/* + * OpenFGA + * A high performance and flexible authorization/permission engine built for developers and inspired by Google Zanzibar. + * + * The version of the OpenAPI document: 0.1 + * Contact: community@openfga.dev + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +package dev.openfga.sdk.api.client; + +import dev.openfga.sdk.api.model.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/src/main/java/dev/openfga/sdk/api/client/ClientTupleKey.java b/src/main/java/dev/openfga/sdk/api/client/ClientTupleKey.java index 6e0b7e1..319c555 100644 --- a/src/main/java/dev/openfga/sdk/api/client/ClientTupleKey.java +++ b/src/main/java/dev/openfga/sdk/api/client/ClientTupleKey.java @@ -12,11 +12,27 @@ package dev.openfga.sdk.api.client; +import dev.openfga.sdk.api.model.TupleKeyWithoutCondition; +import dev.openfga.sdk.api.model.WriteRequestDeletes; +import java.util.Collection; +import java.util.stream.Collectors; + public class ClientTupleKey { 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(ClientTupleKey::asTupleKeyWithoutCondition) + .collect(Collectors.toList())); + } + public ClientTupleKey _object(String _object) { this._object = _object; return this; @@ -55,4 +71,18 @@ public ClientTupleKey user(String user) { public String getUser() { return user; } + + /** + * Adds a condition to the tuple key. + * @param condition a {@link ClientRelationshipCondition} + * @return a new {@link ClientTupleKeyWithCondition} with this {@link ClientTupleKey}'s + * user, relation, and object, and the passed condition. + */ + public ClientTupleKeyWithCondition condition(ClientRelationshipCondition condition) { + return new ClientTupleKeyWithCondition() + .user(user) + .relation(relation) + ._object(_object) + .condition(condition); + } } diff --git a/src/main/java/dev/openfga/sdk/api/client/ClientTupleKeyWithCondition.java b/src/main/java/dev/openfga/sdk/api/client/ClientTupleKeyWithCondition.java new file mode 100644 index 0000000..097f1a8 --- /dev/null +++ b/src/main/java/dev/openfga/sdk/api/client/ClientTupleKeyWithCondition.java @@ -0,0 +1,76 @@ +/* + * OpenFGA + * A high performance and flexible authorization/permission engine built for developers and inspired by Google Zanzibar. + * + * The version of the OpenAPI document: 0.1 + * Contact: community@openfga.dev + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +package dev.openfga.sdk.api.client; + +import dev.openfga.sdk.api.model.ContextualTupleKeys; +import dev.openfga.sdk.api.model.TupleKey; +import dev.openfga.sdk.api.model.WriteRequestWrites; +import java.util.Collection; +import java.util.stream.Collectors; + +public class ClientTupleKeyWithCondition extends ClientTupleKey { + private ClientRelationshipCondition condition; + + public ClientTupleKeyWithCondition condition(ClientRelationshipCondition condition) { + this.condition = condition; + return this; + } + + public ClientRelationshipCondition getCondition() { + return condition; + } + + public TupleKey asTupleKey() { + var tupleKey = new TupleKey().user(getUser()).relation(getRelation())._object(getObject()); + + if (condition != null) { + tupleKey.condition(condition.asRelationshipCondition()); + } + + return tupleKey; + } + + public static ContextualTupleKeys asContextualTupleKeys(Collection tupleKeys) { + return new ContextualTupleKeys() + .tupleKeys(tupleKeys.stream() + .map(ClientTupleKeyWithCondition::asTupleKey) + .collect(Collectors.toList())); + } + + public static WriteRequestWrites asWriteRequestWrites(Collection tupleKeys) { + return new WriteRequestWrites() + .tupleKeys(tupleKeys.stream() + .map(ClientTupleKeyWithCondition::asTupleKey) + .collect(Collectors.toList())); + } + + /* Overrides for correct typing */ + + @Override + public ClientTupleKeyWithCondition user(String user) { + super.user(user); + return this; + } + + @Override + public ClientTupleKeyWithCondition relation(String relation) { + super.relation(relation); + return this; + } + + @Override + public ClientTupleKeyWithCondition _object(String _object) { + super._object(_object); + return this; + } +} diff --git a/src/main/java/dev/openfga/sdk/api/client/ClientWriteRequest.java b/src/main/java/dev/openfga/sdk/api/client/ClientWriteRequest.java index 8dc1f35..189f39a 100644 --- a/src/main/java/dev/openfga/sdk/api/client/ClientWriteRequest.java +++ b/src/main/java/dev/openfga/sdk/api/client/ClientWriteRequest.java @@ -15,19 +15,19 @@ import java.util.List; public class ClientWriteRequest { - private List writes; + private List writes; private List deletes; - public static ClientWriteRequest ofWrites(List writes) { + public static ClientWriteRequest ofWrites(List writes) { return new ClientWriteRequest().writes(writes); } - public ClientWriteRequest writes(List writes) { + public ClientWriteRequest writes(List writes) { this.writes = writes; return this; } - public List getWrites() { + public List getWrites() { return writes; } diff --git a/src/main/java/dev/openfga/sdk/api/client/OpenFgaClient.java b/src/main/java/dev/openfga/sdk/api/client/OpenFgaClient.java index 52e8329..7d89eb6 100644 --- a/src/main/java/dev/openfga/sdk/api/client/OpenFgaClient.java +++ b/src/main/java/dev/openfga/sdk/api/client/OpenFgaClient.java @@ -292,24 +292,12 @@ private CompletableFuture writeNonTransaction( var writeTuples = request.getWrites(); if (writeTuples != null && !writeTuples.isEmpty()) { - body.writes(new WriteRequestWrites() - .tupleKeys(writeTuples.stream() - .map(writeTuple -> new TupleKey() - .user(writeTuple.getUser()) - .relation(writeTuple.getRelation()) - ._object(writeTuple.getObject())) - .collect(Collectors.toList()))); + body.writes(ClientTupleKeyWithCondition.asWriteRequestWrites(writeTuples)); } var deleteTuples = request.getDeletes(); if (deleteTuples != null && !deleteTuples.isEmpty()) { - body.deletes(new WriteRequestDeletes() - .tupleKeys(deleteTuples.stream() - .map(deleteTuple -> new TupleKeyWithoutCondition() - .user(deleteTuple.getUser()) - .relation(deleteTuple.getRelation()) - ._object(deleteTuple.getObject())) - .collect(Collectors.toList()))); + body.deletes(ClientTupleKey.asWriteRequestDeletes(deleteTuples)); } if (options != null && !isNullOrWhitespace(options.getAuthorizationModelId())) { @@ -369,20 +357,14 @@ private Stream> chunksOf(int chunkSize, List list) { * * @throws FgaInvalidParameterException When the Store ID is null, empty, or whitespace */ - public CompletableFuture writeTuples(List tupleKeys) + public CompletableFuture writeTuples(List tupleKeys) throws FgaInvalidParameterException { configuration.assertValid(); String storeId = configuration.getStoreIdChecked(); var body = new WriteRequest(); - body.writes(new WriteRequestWrites() - .tupleKeys(tupleKeys.stream() - .map(writeTuple -> new TupleKey() - .user(writeTuple.getUser()) - .relation(writeTuple.getRelation()) - ._object(writeTuple.getObject())) - .collect(Collectors.toList()))); + body.writes(ClientTupleKeyWithCondition.asWriteRequestWrites(tupleKeys)); String authorizationModelId = configuration.getAuthorizationModelId(); if (!isNullOrWhitespace(authorizationModelId)) { @@ -404,13 +386,7 @@ public CompletableFuture deleteTuples(List var body = new WriteRequest(); - body.deletes(new WriteRequestDeletes() - .tupleKeys(tupleKeys.stream() - .map(deleteTuple -> new TupleKeyWithoutCondition() - .user(deleteTuple.getUser()) - .relation(deleteTuple.getRelation()) - ._object(deleteTuple.getObject())) - .collect(Collectors.toList()))); + body.deletes(ClientTupleKey.asWriteRequestDeletes(tupleKeys)); String authorizationModelId = configuration.getAuthorizationModelId(); if (!isNullOrWhitespace(authorizationModelId)) { @@ -447,20 +423,11 @@ public CompletableFuture check(ClientCheckRequest request, CheckRequest body = new CheckRequest(); if (request != null) { - body.tupleKey(new CheckRequestTupleKey() - .user(request.getUser()) - .relation(request.getRelation()) - ._object(request.getObject())); + body.tupleKey(request.asCheckRequestTupleKey()); var contextualTuples = request.getContextualTuples(); if (contextualTuples != null && !contextualTuples.isEmpty()) { - body.contextualTuples(new ContextualTupleKeys() - .tupleKeys(contextualTuples.stream() - .map(contextualTuple -> new TupleKey() - ._object(contextualTuple.getObject()) - .relation(contextualTuple.getRelation()) - .user(contextualTuple.getUser())) - .collect(Collectors.toList()))); + body.contextualTuples(ClientTupleKeyWithCondition.asContextualTupleKeys(contextualTuples)); } } @@ -571,13 +538,9 @@ public CompletableFuture listObjects( if (request != null) { body.user(request.getUser()).relation(request.getRelation()).type(request.getType()); if (request.getContextualTupleKeys() != null) { - body.contextualTuples(new ContextualTupleKeys() - .tupleKeys(request.getContextualTupleKeys().stream() - .map(contextualTuple -> new TupleKey() - ._object(contextualTuple.getObject()) - .relation(contextualTuple.getRelation()) - .user(contextualTuple.getUser())) - .collect(Collectors.toList()))); + var contextualTuples = request.getContextualTupleKeys(); + var bodyContextualTuples = ClientTupleKeyWithCondition.asContextualTupleKeys(contextualTuples); + body.contextualTuples(bodyContextualTuples); } } diff --git a/src/main/java/dev/openfga/sdk/api/model/AuthorizationModel.java b/src/main/java/dev/openfga/sdk/api/model/AuthorizationModel.java index ded98d6..d85cafd 100644 --- a/src/main/java/dev/openfga/sdk/api/model/AuthorizationModel.java +++ b/src/main/java/dev/openfga/sdk/api/model/AuthorizationModel.java @@ -57,15 +57,15 @@ public AuthorizationModel id(String id) { * Get id * @return id **/ - @javax.annotation.Nullable + @javax.annotation.Nonnull @JsonProperty(JSON_PROPERTY_ID) - @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + @JsonInclude(value = JsonInclude.Include.ALWAYS) public String getId() { return id; } @JsonProperty(JSON_PROPERTY_ID) - @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + @JsonInclude(value = JsonInclude.Include.ALWAYS) public void setId(String id) { this.id = id; } diff --git a/src/main/java/dev/openfga/sdk/api/model/NullValue.java b/src/main/java/dev/openfga/sdk/api/model/NullValue.java index d72381e..d2718bf 100644 --- a/src/main/java/dev/openfga/sdk/api/model/NullValue.java +++ b/src/main/java/dev/openfga/sdk/api/model/NullValue.java @@ -16,7 +16,7 @@ import com.fasterxml.jackson.annotation.JsonValue; /** - * `NullValue` is a singleton enumeration to represent the null value for the `Value` type union. The JSON representation for `NullValue` is JSON `null`. - NULL_VALUE: Null value. + * `NullValue` is a singleton enumeration to represent the null value for the `Value` type union. The JSON representation for `NullValue` is JSON `null`. - NULL_VALUE: Null value. */ public enum NullValue { NULL_VALUE("NULL_VALUE"), diff --git a/src/main/java/dev/openfga/sdk/api/model/RelationshipCondition.java b/src/main/java/dev/openfga/sdk/api/model/RelationshipCondition.java index 33f50b1..e4a9f88 100644 --- a/src/main/java/dev/openfga/sdk/api/model/RelationshipCondition.java +++ b/src/main/java/dev/openfga/sdk/api/model/RelationshipCondition.java @@ -64,15 +64,15 @@ public RelationshipCondition context(Object context) { * Additional context/data to persist along with the condition. The keys must match the parameters defined by the condition, and the value types must match the parameter type definitions. * @return context **/ - @javax.annotation.Nonnull + @javax.annotation.Nullable @JsonProperty(JSON_PROPERTY_CONTEXT) - @JsonInclude(value = JsonInclude.Include.ALWAYS) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) public Object getContext() { return context; } @JsonProperty(JSON_PROPERTY_CONTEXT) - @JsonInclude(value = JsonInclude.Include.ALWAYS) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) public void setContext(Object context) { this.context = context; } diff --git a/src/test-integration/java/dev/openfga/sdk/api/client/OpenFgaClientIntegrationTest.java b/src/test-integration/java/dev/openfga/sdk/api/client/OpenFgaClientIntegrationTest.java index 0434de5..374169e 100644 --- a/src/test-integration/java/dev/openfga/sdk/api/client/OpenFgaClientIntegrationTest.java +++ b/src/test-integration/java/dev/openfga/sdk/api/client/OpenFgaClientIntegrationTest.java @@ -19,6 +19,7 @@ import dev.openfga.sdk.api.configuration.*; import dev.openfga.sdk.api.model.*; import java.util.List; +import java.util.Map; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -28,10 +29,11 @@ public class OpenFgaClientIntegrationTest { "{\"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 = + private 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 = new ClientAssertion() + private static final ClientRelationshipCondition DEFAULT_CONDITION = + new ClientRelationshipCondition().name("condition").context(Map.of("some", "context")); + private static final ClientAssertion DEFAULT_ASSERTION = new ClientAssertion() .user(DEFAULT_USER) .relation("reader") ._object(DEFAULT_DOC) @@ -196,7 +198,8 @@ public void write_and_read() throws Exception { String authModelId = writeAuthModel(storeId); fga.setAuthorizationModelId(authModelId); - ClientWriteRequest writeRequest = new ClientWriteRequest().writes(List.of(DEFAULT_TUPLE_KEY)); + ClientWriteRequest writeRequest = + new ClientWriteRequest().writes(List.of(DEFAULT_TUPLE_KEY.condition(DEFAULT_CONDITION))); ClientReadRequest readRequest = new ClientReadRequest().user(DEFAULT_USER)._object(DEFAULT_DOC); @@ -221,7 +224,8 @@ public void write_and_check() throws Exception { fga.setStoreId(storeId); String authModelId = writeAuthModel(storeId); fga.setAuthorizationModelId(authModelId); - ClientWriteRequest writeRequest = new ClientWriteRequest().writes(List.of(DEFAULT_TUPLE_KEY)); + ClientWriteRequest writeRequest = + new ClientWriteRequest().writes(List.of(DEFAULT_TUPLE_KEY.condition(DEFAULT_CONDITION))); ClientCheckRequest checkRequest = new ClientCheckRequest().user(DEFAULT_USER).relation("reader")._object(DEFAULT_DOC); @@ -242,7 +246,8 @@ public void write_and_expand() throws Exception { fga.setStoreId(storeId); String authModelId = writeAuthModel(storeId); fga.setAuthorizationModelId(authModelId); - ClientWriteRequest writeRequest = new ClientWriteRequest().writes(List.of(DEFAULT_TUPLE_KEY)); + ClientWriteRequest writeRequest = + new ClientWriteRequest().writes(List.of(DEFAULT_TUPLE_KEY.condition(DEFAULT_CONDITION))); ClientExpandRequest expandRequest = new ClientExpandRequest()._object(DEFAULT_DOC).relation("reader"); @@ -272,7 +277,8 @@ public void write_and_listObjects() throws Exception { fga.setStoreId(storeId); String authModelId = writeAuthModel(storeId); fga.setAuthorizationModelId(authModelId); - ClientWriteRequest writeRequest = new ClientWriteRequest().writes(List.of(DEFAULT_TUPLE_KEY)); + ClientWriteRequest writeRequest = + new ClientWriteRequest().writes(List.of(DEFAULT_TUPLE_KEY.condition(DEFAULT_CONDITION))); ClientListObjectsRequest listObjectsRequest = new ClientListObjectsRequest() .user(DEFAULT_USER) .relation("reader") diff --git a/src/test/java/dev/openfga/sdk/api/client/OpenFgaClientTest.java b/src/test/java/dev/openfga/sdk/api/client/OpenFgaClientTest.java index 89dc12b..157c904 100644 --- a/src/test/java/dev/openfga/sdk/api/client/OpenFgaClientTest.java +++ b/src/test/java/dev/openfga/sdk/api/client/OpenFgaClientTest.java @@ -24,6 +24,7 @@ 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; @@ -45,6 +46,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); @@ -1055,7 +1058,7 @@ public void writeTest_writes() throws Exception { 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() + .writes(List.of(new ClientTupleKeyWithCondition() ._object(DEFAULT_OBJECT) .relation(DEFAULT_RELATION) .user(DEFAULT_USER))); @@ -1097,12 +1100,13 @@ public void writeTest_transactions() throws Exception { // Given String postPath = "https://localhost/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write"; String writeTupleBody = String.format( - "{\"user\":\"%s\",\"relation\":\"%s\",\"object\":\"%s\",\"condition\":null}", + "{\"user\":\"%s\",\"relation\":\"%s\",\"object\":\"%s\",\"condition\":{\"name\":\"condition\",\"context\":{\"some\":\"context\"}}}", DEFAULT_USER, DEFAULT_RELATION, DEFAULT_OBJECT); ClientTupleKey tuple = new ClientTupleKey() ._object(DEFAULT_OBJECT) .relation(DEFAULT_RELATION) .user(DEFAULT_USER); + ClientTupleKeyWithCondition writeTuple = tuple.condition(DEFAULT_CONDITION); String write2Body = String.format( "{\"writes\":{\"tuple_keys\":[%s,%s]},\"deletes\":null,\"authorization_model_id\":\"%s\"}", writeTupleBody, writeTupleBody, DEFAULT_AUTH_MODEL_ID); @@ -1123,7 +1127,7 @@ public void writeTest_transactions() throws Exception { .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); @@ -1147,7 +1151,7 @@ public void writeTest_transactionsWithFailure() throws Exception { String failedUser = "user:SECOND"; String skippedUser = "user:third"; Function writeBody = user -> String.format( - "{\"writes\":{\"tuple_keys\":[{\"user\":\"%s\",\"relation\":\"%s\",\"object\":\"%s\",\"condition\":null}]},\"deletes\":null,\"authorization_model_id\":\"%s\"}", + "{\"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) @@ -1159,10 +1163,11 @@ public void writeTest_transactionsWithFailure() throws Exception { .doReturn(400, "{\"code\":\"validation_error\",\"message\":\"Generic validation error\"}"); ClientWriteRequest request = new ClientWriteRequest() .writes(Stream.of(firstUser, failedUser, skippedUser) - .map(user -> new ClientTupleKey() + .map(user -> new ClientTupleKeyWithCondition() ._object(DEFAULT_OBJECT) .relation(DEFAULT_RELATION) - .user(user)) + .user(user) + .condition(DEFAULT_CONDITION)) .collect(Collectors.toList())); ClientWriteOptions options = new ClientWriteOptions().disableTransactions(false).transactionChunkSize(1); @@ -1196,7 +1201,7 @@ public void writeTest_nonTransaction() throws Exception { // Given String postPath = "https://localhost/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write"; String writeTupleBody = String.format( - "{\"user\":\"%s\",\"relation\":\"%s\",\"object\":\"%s\",\"condition\":null}", + "{\"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\"}", @@ -1211,10 +1216,11 @@ public void writeTest_nonTransaction() throws Exception { deleteTupleBody, DEFAULT_AUTH_MODEL_ID); mockHttpClient.onPost(postPath).withBody(is(expectedBody)).doReturn(200, EMPTY_RESPONSE_BODY); - ClientTupleKey tuple = new ClientTupleKey() + var tuple = new ClientTupleKeyWithCondition() ._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)); @@ -1235,7 +1241,7 @@ public void writeTest_nonTransactionsWithFailure() throws Exception { // Given String postPath = "https://localhost/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write"; String writeTupleBody = String.format( - "{\"user\":\"%s\",\"relation\":\"%s\",\"object\":\"%s\",\"condition\":null}", + "{\"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\"}", @@ -1253,10 +1259,11 @@ public void writeTest_nonTransactionsWithFailure() throws Exception { .onPost(postPath) .withBody(is(expectedBody)) .doReturn(400, "{\"code\":\"validation_error\",\"message\":\"Generic validation error\"}"); - ClientTupleKey tuple = new ClientTupleKey() + var tuple = new ClientTupleKeyWithCondition() ._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)); @@ -1279,13 +1286,15 @@ public void writeTuplesTest() throws Exception { // Given String postPath = String.format("https://localhost/stores/%s/write", DEFAULT_STORE_ID); String expectedBody = String.format( - "{\"writes\":{\"tuple_keys\":[{\"user\":\"%s\",\"relation\":\"%s\",\"object\":\"%s\",\"condition\":null}]},\"deletes\":null,\"authorization_model_id\":\"%s\"}", + "{\"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() + List tuples = List.of(new ClientTupleKeyWithCondition() ._object(DEFAULT_OBJECT) .relation(DEFAULT_RELATION) - .user(DEFAULT_USER)); + .user(DEFAULT_USER) + .condition(DEFAULT_CONDITION)); // When ClientWriteResponse response = fga.writeTuples(tuples).get(); @@ -1338,10 +1347,11 @@ public void write_400() throws Exception { .onPost(postUrl) .doReturn(400, "{\"code\":\"validation_error\",\"message\":\"Generic validation error\"}"); ClientWriteRequest request = new ClientWriteRequest() - .writes(List.of(new ClientTupleKey() + .writes(List.of(new ClientTupleKeyWithCondition() ._object(DEFAULT_OBJECT) .relation(DEFAULT_RELATION) - .user(DEFAULT_USER))); + .user(DEFAULT_USER) + .condition(DEFAULT_CONDITION))); // When ExecutionException execException = @@ -1364,10 +1374,11 @@ public void write_404() throws Exception { .onPost(postUrl) .doReturn(404, "{\"code\":\"undefined_endpoint\",\"message\":\"Endpoint not enabled\"}"); ClientWriteRequest request = new ClientWriteRequest() - .writes(List.of(new ClientTupleKey() + .writes(List.of(new ClientTupleKeyWithCondition() ._object(DEFAULT_OBJECT) .relation(DEFAULT_RELATION) - .user(DEFAULT_USER))); + .user(DEFAULT_USER) + .condition(DEFAULT_CONDITION))); // When ExecutionException execException = @@ -1389,10 +1400,11 @@ public void write_500() throws Exception { .onPost(postUrl) .doReturn(500, "{\"code\":\"internal_error\",\"message\":\"Internal Server Error\"}"); ClientWriteRequest request = new ClientWriteRequest() - .writes(List.of(new ClientTupleKey() + .writes(List.of(new ClientTupleKeyWithCondition() ._object(DEFAULT_OBJECT) .relation(DEFAULT_RELATION) - .user(DEFAULT_USER))); + .user(DEFAULT_USER) + .condition(DEFAULT_CONDITION))); // When ExecutionException execException = @@ -1415,7 +1427,7 @@ public void check() throws Exception { String postUrl = String.format("https://localhost/stores/%s/check", DEFAULT_STORE_ID); String expectedBody = String.format( "{\"tuple_key\":{\"user\":\"%s\",\"relation\":\"%s\",\"object\":\"%s\"}," - + "\"contextual_tuples\":{\"tuple_keys\":[{\"user\":\"%s\",\"relation\":\"owner\",\"object\":\"%s\",\"condition\":null}]}," + + "\"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}"); @@ -1423,10 +1435,11 @@ public void check() throws Exception { ._object(DEFAULT_OBJECT) .relation(DEFAULT_RELATION) .user(DEFAULT_USER) - .contextualTuples(List.of(new ClientTupleKey() + .contextualTuples(List.of(new ClientTupleKeyWithCondition() ._object(DEFAULT_OBJECT) .relation("owner") - .user(DEFAULT_USER))); + .user(DEFAULT_USER) + .condition(DEFAULT_CONDITION))); ClientCheckOptions options = new ClientCheckOptions().authorizationModelId(DEFAULT_AUTH_MODEL_ID); // When