From 72b2453b900a28a78e6ef463ff3e00e4ecbcedd4 Mon Sep 17 00:00:00 2001 From: Mary Dickson Date: Mon, 20 Apr 2026 17:12:06 -0700 Subject: [PATCH 1/2] feat(sdk): add ergonomic Resource constructors for authorization Add Resources utility class with forAttributeValues and forRegisteredResourceValueFqn helpers to reduce boilerplate when building Resource objects for authorization calls. Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Mary Dickson --- .../io/opentdf/platform/sdk/Resources.java | 71 +++++++++++++++++++ .../opentdf/platform/sdk/ResourcesTest.java | 63 ++++++++++++++++ 2 files changed, 134 insertions(+) create mode 100644 sdk/src/main/java/io/opentdf/platform/sdk/Resources.java create mode 100644 sdk/src/test/java/io/opentdf/platform/sdk/ResourcesTest.java diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/Resources.java b/sdk/src/main/java/io/opentdf/platform/sdk/Resources.java new file mode 100644 index 00000000..9f862c69 --- /dev/null +++ b/sdk/src/main/java/io/opentdf/platform/sdk/Resources.java @@ -0,0 +1,71 @@ +package io.opentdf.platform.sdk; + +import io.opentdf.platform.authorization.v2.Resource; + +import java.util.Arrays; +import java.util.Objects; + +/** + * Convenience constructors for {@link Resource}, mirroring the Go SDK helpers + * in {@code authorization/v2.ForAttributeValues}, {@code ForRegisteredResourceValueFqn}. + * + *

Each method builds the full {@code Resource} proto so callers avoid + * deeply nested builder chains. + * + *

{@code
+ * // Before
+ * Resource.newBuilder()
+ *     .setAttributeValues(
+ *         Resource.AttributeValues.newBuilder()
+ *             .addFqns("https://example.com/attr/department/value/finance"))
+ *     .build();
+ *
+ * // After
+ * Resources.forAttributeValues("https://example.com/attr/department/value/finance");
+ * }
+ */ +public final class Resources { + + private Resources() {} + + /** + * Returns a Resource containing the given attribute value FQNs. + * This is the most common Resource variant, used when authorizing against + * attribute values attached to data (e.g. those on a TDF). + * + * @param fqns one or more fully qualified attribute value names + * @return a fully built {@link Resource} with the {@code attribute_values} oneof set + * @throws NullPointerException if {@code fqns} or any element is null + * @throws IllegalArgumentException if {@code fqns} is empty + */ + public static Resource forAttributeValues(String... fqns) { + Objects.requireNonNull(fqns, "fqns must not be null"); + if (fqns.length == 0) { + throw new IllegalArgumentException("fqns must not be empty"); + } + for (String fqn : fqns) { + Objects.requireNonNull(fqn, "individual fqn must not be null"); + } + return Resource.newBuilder() + .setAttributeValues( + Resource.AttributeValues.newBuilder() + .addAllFqns(Arrays.asList(fqns)) + .build()) + .build(); + } + + /** + * Returns a Resource that references a single registered resource value + * by its fully qualified name, as stored in platform policy. + * + * @param fqn the fully qualified name of the registered resource value + * @return a fully built {@link Resource} with the {@code registered_resource_value_fqn} oneof set + * @throws NullPointerException if {@code fqn} is null + */ + public static Resource forRegisteredResourceValueFqn(String fqn) { + Objects.requireNonNull(fqn, "fqn must not be null"); + return Resource.newBuilder() + .setRegisteredResourceValueFqn(fqn) + .build(); + } +} diff --git a/sdk/src/test/java/io/opentdf/platform/sdk/ResourcesTest.java b/sdk/src/test/java/io/opentdf/platform/sdk/ResourcesTest.java new file mode 100644 index 00000000..f1bb5488 --- /dev/null +++ b/sdk/src/test/java/io/opentdf/platform/sdk/ResourcesTest.java @@ -0,0 +1,63 @@ +package io.opentdf.platform.sdk; + +import static org.junit.jupiter.api.Assertions.*; + +import io.opentdf.platform.authorization.v2.Resource; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class ResourcesTest { + + @Test + void forAttributeValues_single() { + String fqn = "https://example.com/attr/department/value/finance"; + Resource r = Resources.forAttributeValues(fqn); + + assertEquals(Resource.ResourceCase.ATTRIBUTE_VALUES, r.getResourceCase()); + assertEquals(1, r.getAttributeValues().getFqnsCount()); + assertEquals(fqn, r.getAttributeValues().getFqns(0)); + } + + @Test + void forAttributeValues_multiple() { + String fqn1 = "https://example.com/attr/department/value/finance"; + String fqn2 = "https://example.com/attr/level/value/public"; + Resource r = Resources.forAttributeValues(fqn1, fqn2); + + assertEquals(Resource.ResourceCase.ATTRIBUTE_VALUES, r.getResourceCase()); + assertEquals(2, r.getAttributeValues().getFqnsCount()); + assertEquals(fqn1, r.getAttributeValues().getFqns(0)); + assertEquals(fqn2, r.getAttributeValues().getFqns(1)); + } + + @Test + void forAttributeValues_emptyStringFqn() { + Resource r = Resources.forAttributeValues(""); + + assertEquals(Resource.ResourceCase.ATTRIBUTE_VALUES, r.getResourceCase()); + assertEquals(1, r.getAttributeValues().getFqnsCount()); + assertEquals("", r.getAttributeValues().getFqns(0)); + } + + @Test + void forAttributeValues_emptyArrayThrows() { + assertThrows(IllegalArgumentException.class, () -> Resources.forAttributeValues()); + } + + @ParameterizedTest + @ValueSource(strings = {"https://example.com/attr/department/value/finance", ""}) + void forRegisteredResourceValueFqn(String fqn) { + Resource r = Resources.forRegisteredResourceValueFqn(fqn); + + assertEquals(Resource.ResourceCase.REGISTERED_RESOURCE_VALUE_FQN, r.getResourceCase()); + assertEquals(fqn, r.getRegisteredResourceValueFqn()); + } + + @Test + void nullInputsThrow() { + assertThrows(NullPointerException.class, () -> Resources.forAttributeValues((String[]) null)); + assertThrows(NullPointerException.class, () -> Resources.forAttributeValues("valid", null)); + assertThrows(NullPointerException.class, () -> Resources.forRegisteredResourceValueFqn(null)); + } +} From 4690691c8746f2111e667104eb6f7ae222d3e6b3 Mon Sep 17 00:00:00 2001 From: Mary Dickson Date: Tue, 21 Apr 2026 07:35:05 -0700 Subject: [PATCH 2/2] fix(sdk): fix Resources Javadoc to reference EntityIdentifiers Update class-level Javadoc to reference EntityIdentifiers instead of non-existent Go SDK helpers. Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Mary Dickson --- sdk/src/main/java/io/opentdf/platform/sdk/Resources.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/Resources.java b/sdk/src/main/java/io/opentdf/platform/sdk/Resources.java index 9f862c69..527bec7f 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/Resources.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/Resources.java @@ -6,8 +6,8 @@ import java.util.Objects; /** - * Convenience constructors for {@link Resource}, mirroring the Go SDK helpers - * in {@code authorization/v2.ForAttributeValues}, {@code ForRegisteredResourceValueFqn}. + * Convenience constructors for {@link Resource}, analogous to the + * {@link EntityIdentifiers} helpers for {@link io.opentdf.platform.authorization.v2.EntityIdentifier}. * *

Each method builds the full {@code Resource} proto so callers avoid * deeply nested builder chains.