Skip to content

Commit 9e05ca2

Browse files
marythoughtclaude
andauthored
feat(sdk): add ergonomic Resource constructors for authorization (#354)
## Summary - Add `Resources` utility class with `forAttributeValues(String... fqns)` and `forRegisteredResourceValueFqn(String fqn)` helpers - Follows the same pattern as `EntityIdentifiers` from #346 - Reduces verbose nested protobuf builder chains to single-line calls **Before:** ```java Resource.newBuilder() .setAttributeValues( Resource.AttributeValues.newBuilder() .addFqns("https://example.com/attr/department/value/finance")) .build(); ``` **After:** ```java Resources.forAttributeValues("https://example.com/attr/department/value/finance"); ``` **Example — GetDecision authorization call:** ```java import io.opentdf.platform.sdk.EntityIdentifiers; import io.opentdf.platform.sdk.Resources; GetDecisionRequest request = GetDecisionRequest.newBuilder() .setEntityIdentifier(EntityIdentifiers.forEmail("user@company.com")) .setAction(Action.newBuilder().setName("decrypt")) .setResource(Resources.forAttributeValues( "https://company.com/attr/clearance/value/confidential", "https://company.com/attr/department/value/finance")) .build(); GetDecisionResponse resp = sdk.getServices() .authorization() .getDecision(request) .get(); ``` ## Test plan - [x] Unit tests for `forAttributeValues` (single, multiple, empty-string FQN, empty array throws) - [x] Unit tests for `forRegisteredResourceValueFqn` (valid + empty string) - [x] Null safety tests (null array, null element, null fqn all throw NPE) - [x] Full SDK test suite passes (157 tests, 0 failures) - [x] CI checks pass 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Mary Dickson <mary.dickson@virtru.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 2c55e31 commit 9e05ca2

2 files changed

Lines changed: 134 additions & 0 deletions

File tree

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package io.opentdf.platform.sdk;
2+
3+
import io.opentdf.platform.authorization.v2.Resource;
4+
5+
import java.util.Arrays;
6+
import java.util.Objects;
7+
8+
/**
9+
* Convenience constructors for {@link Resource}, analogous to the
10+
* {@link EntityIdentifiers} helpers for {@link io.opentdf.platform.authorization.v2.EntityIdentifier}.
11+
*
12+
* <p>Each method builds the full {@code Resource} proto so callers avoid
13+
* deeply nested builder chains.
14+
*
15+
* <pre>{@code
16+
* // Before
17+
* Resource.newBuilder()
18+
* .setAttributeValues(
19+
* Resource.AttributeValues.newBuilder()
20+
* .addFqns("https://example.com/attr/department/value/finance"))
21+
* .build();
22+
*
23+
* // After
24+
* Resources.forAttributeValues("https://example.com/attr/department/value/finance");
25+
* }</pre>
26+
*/
27+
public final class Resources {
28+
29+
private Resources() {}
30+
31+
/**
32+
* Returns a Resource containing the given attribute value FQNs.
33+
* This is the most common Resource variant, used when authorizing against
34+
* attribute values attached to data (e.g. those on a TDF).
35+
*
36+
* @param fqns one or more fully qualified attribute value names
37+
* @return a fully built {@link Resource} with the {@code attribute_values} oneof set
38+
* @throws NullPointerException if {@code fqns} or any element is null
39+
* @throws IllegalArgumentException if {@code fqns} is empty
40+
*/
41+
public static Resource forAttributeValues(String... fqns) {
42+
Objects.requireNonNull(fqns, "fqns must not be null");
43+
if (fqns.length == 0) {
44+
throw new IllegalArgumentException("fqns must not be empty");
45+
}
46+
for (String fqn : fqns) {
47+
Objects.requireNonNull(fqn, "individual fqn must not be null");
48+
}
49+
return Resource.newBuilder()
50+
.setAttributeValues(
51+
Resource.AttributeValues.newBuilder()
52+
.addAllFqns(Arrays.asList(fqns))
53+
.build())
54+
.build();
55+
}
56+
57+
/**
58+
* Returns a Resource that references a single registered resource value
59+
* by its fully qualified name, as stored in platform policy.
60+
*
61+
* @param fqn the fully qualified name of the registered resource value
62+
* @return a fully built {@link Resource} with the {@code registered_resource_value_fqn} oneof set
63+
* @throws NullPointerException if {@code fqn} is null
64+
*/
65+
public static Resource forRegisteredResourceValueFqn(String fqn) {
66+
Objects.requireNonNull(fqn, "fqn must not be null");
67+
return Resource.newBuilder()
68+
.setRegisteredResourceValueFqn(fqn)
69+
.build();
70+
}
71+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package io.opentdf.platform.sdk;
2+
3+
import static org.junit.jupiter.api.Assertions.*;
4+
5+
import io.opentdf.platform.authorization.v2.Resource;
6+
import org.junit.jupiter.api.Test;
7+
import org.junit.jupiter.params.ParameterizedTest;
8+
import org.junit.jupiter.params.provider.ValueSource;
9+
10+
class ResourcesTest {
11+
12+
@Test
13+
void forAttributeValues_single() {
14+
String fqn = "https://example.com/attr/department/value/finance";
15+
Resource r = Resources.forAttributeValues(fqn);
16+
17+
assertEquals(Resource.ResourceCase.ATTRIBUTE_VALUES, r.getResourceCase());
18+
assertEquals(1, r.getAttributeValues().getFqnsCount());
19+
assertEquals(fqn, r.getAttributeValues().getFqns(0));
20+
}
21+
22+
@Test
23+
void forAttributeValues_multiple() {
24+
String fqn1 = "https://example.com/attr/department/value/finance";
25+
String fqn2 = "https://example.com/attr/level/value/public";
26+
Resource r = Resources.forAttributeValues(fqn1, fqn2);
27+
28+
assertEquals(Resource.ResourceCase.ATTRIBUTE_VALUES, r.getResourceCase());
29+
assertEquals(2, r.getAttributeValues().getFqnsCount());
30+
assertEquals(fqn1, r.getAttributeValues().getFqns(0));
31+
assertEquals(fqn2, r.getAttributeValues().getFqns(1));
32+
}
33+
34+
@Test
35+
void forAttributeValues_emptyStringFqn() {
36+
Resource r = Resources.forAttributeValues("");
37+
38+
assertEquals(Resource.ResourceCase.ATTRIBUTE_VALUES, r.getResourceCase());
39+
assertEquals(1, r.getAttributeValues().getFqnsCount());
40+
assertEquals("", r.getAttributeValues().getFqns(0));
41+
}
42+
43+
@Test
44+
void forAttributeValues_emptyArrayThrows() {
45+
assertThrows(IllegalArgumentException.class, () -> Resources.forAttributeValues());
46+
}
47+
48+
@ParameterizedTest
49+
@ValueSource(strings = {"https://example.com/attr/department/value/finance", ""})
50+
void forRegisteredResourceValueFqn(String fqn) {
51+
Resource r = Resources.forRegisteredResourceValueFqn(fqn);
52+
53+
assertEquals(Resource.ResourceCase.REGISTERED_RESOURCE_VALUE_FQN, r.getResourceCase());
54+
assertEquals(fqn, r.getRegisteredResourceValueFqn());
55+
}
56+
57+
@Test
58+
void nullInputsThrow() {
59+
assertThrows(NullPointerException.class, () -> Resources.forAttributeValues((String[]) null));
60+
assertThrows(NullPointerException.class, () -> Resources.forAttributeValues("valid", null));
61+
assertThrows(NullPointerException.class, () -> Resources.forRegisteredResourceValueFqn(null));
62+
}
63+
}

0 commit comments

Comments
 (0)