|
1 | 1 | package io.opentdf.platform.sdk; |
2 | 2 |
|
3 | 3 | import com.connectrpc.Interceptor; |
4 | | - |
| 4 | +import com.connectrpc.ResponseMessageKt; |
5 | 5 | import com.connectrpc.impl.ProtocolClient; |
6 | 6 | import io.opentdf.platform.authorization.AuthorizationServiceClientInterface; |
| 7 | +import io.opentdf.platform.authorization.Entity; |
| 8 | +import io.opentdf.platform.authorization.EntityEntitlements; |
| 9 | +import io.opentdf.platform.authorization.GetEntitlementsRequest; |
| 10 | +import io.opentdf.platform.authorization.GetEntitlementsResponse; |
| 11 | +import io.opentdf.platform.policy.Attribute; |
| 12 | +import io.opentdf.platform.policy.PageRequest; |
7 | 13 | import io.opentdf.platform.policy.SimpleKasKey; |
| 14 | +import io.opentdf.platform.policy.Value; |
8 | 15 | import io.opentdf.platform.policy.attributes.AttributesServiceClientInterface; |
| 16 | +import io.opentdf.platform.policy.attributes.GetAttributeRequest; |
| 17 | +import io.opentdf.platform.policy.attributes.GetAttributeValuesByFqnsRequest; |
| 18 | +import io.opentdf.platform.policy.attributes.GetAttributeValuesByFqnsResponse; |
| 19 | +import io.opentdf.platform.policy.attributes.ListAttributesRequest; |
9 | 20 | import io.opentdf.platform.policy.kasregistry.KeyAccessServerRegistryServiceClientInterface; |
10 | 21 | import io.opentdf.platform.policy.namespaces.NamespaceServiceClientInterface; |
11 | 22 | import io.opentdf.platform.policy.resourcemapping.ResourceMappingServiceClientInterface; |
|
17 | 28 | import java.io.InputStream; |
18 | 29 | import java.io.OutputStream; |
19 | 30 | import java.nio.channels.SeekableByteChannel; |
| 31 | +import java.util.ArrayList; |
| 32 | +import java.util.Collections; |
| 33 | +import java.util.List; |
| 34 | +import java.util.Map; |
20 | 35 | import java.util.Optional; |
| 36 | +import java.util.stream.Collectors; |
21 | 37 |
|
22 | 38 | /** |
23 | 39 | * The SDK class represents a software development kit for interacting with the |
|
26 | 42 | * platform. |
27 | 43 | */ |
28 | 44 | public class SDK implements AutoCloseable { |
| 45 | + |
| 46 | + // Caps the pagination loop in listAttributes to prevent unbounded memory growth |
| 47 | + // if a server repeatedly returns a non-zero next_offset. |
| 48 | + private static final int MAX_LIST_ATTRIBUTES_PAGES = 1000; |
| 49 | + |
| 50 | + // Matches the server-side limit on GetAttributeValuesByFqns so callers get a |
| 51 | + // clear local error instead of a cryptic server rejection. |
| 52 | + private static final int MAX_VALIDATE_FQNS = 250; |
| 53 | + |
29 | 54 | private final Services services; |
30 | 55 | private final TrustManager trustManager; |
31 | 56 | private final Interceptor authInterceptor; |
@@ -179,6 +204,172 @@ public String getPlatformUrl() { |
179 | 204 | return platformUrl; |
180 | 205 | } |
181 | 206 |
|
| 207 | + /** |
| 208 | + * Lists all active attributes available on the platform, auto-paginating through all results. |
| 209 | + * An optional namespace name or ID may be provided to filter results. |
| 210 | + * |
| 211 | + * <p>Use this before calling {@code createTDF()} to see what attributes are available for data tagging. |
| 212 | + * |
| 213 | + * @param namespace optional namespace name or ID to filter results |
| 214 | + * @return list of all active {@link Attribute} objects |
| 215 | + * @throws SDKException if a service error occurs or pagination exceeds the maximum page limit |
| 216 | + */ |
| 217 | + public List<Attribute> listAttributes(String... namespace) { |
| 218 | + ListAttributesRequest.Builder reqBuilder = ListAttributesRequest.newBuilder(); |
| 219 | + if (namespace.length > 0 && namespace[0] != null) { |
| 220 | + reqBuilder.setNamespace(namespace[0]); |
| 221 | + } |
| 222 | + List<Attribute> result = new ArrayList<>(); |
| 223 | + for (int pages = 0; pages < MAX_LIST_ATTRIBUTES_PAGES; pages++) { |
| 224 | + var resp = ResponseMessageKt.getOrThrow( |
| 225 | + services.attributes() |
| 226 | + .listAttributesBlocking(reqBuilder.build(), Collections.emptyMap()) |
| 227 | + .execute()); |
| 228 | + result.addAll(resp.getAttributesList()); |
| 229 | + int nextOffset = resp.getPagination().getNextOffset(); |
| 230 | + if (nextOffset == 0) { |
| 231 | + return result; |
| 232 | + } |
| 233 | + reqBuilder.setPagination(PageRequest.newBuilder().setOffset(nextOffset).build()); |
| 234 | + } |
| 235 | + throw new SDKException("listing attributes: exceeded maximum page limit (" + MAX_LIST_ATTRIBUTES_PAGES + ")"); |
| 236 | + } |
| 237 | + |
| 238 | + /** |
| 239 | + * Checks that all provided attribute value FQNs exist on the platform. |
| 240 | + * Validates FQN format first, then verifies existence via the platform API. |
| 241 | + * |
| 242 | + * <p>Use this before {@code createTDF()} to catch missing or misspelled attributes early |
| 243 | + * instead of discovering the problem at decryption time. |
| 244 | + * |
| 245 | + * @param fqns list of attribute value FQNs in the form |
| 246 | + * {@code https://<namespace>/attr/<name>/value/<value>} |
| 247 | + * @throws AttributeNotFoundException if any FQNs are not found on the platform |
| 248 | + * @throws SDKException if input validation fails or a service error occurs |
| 249 | + */ |
| 250 | + public void validateAttributes(List<String> fqns) { |
| 251 | + if (fqns == null || fqns.isEmpty()) { |
| 252 | + return; |
| 253 | + } |
| 254 | + if (fqns.size() > MAX_VALIDATE_FQNS) { |
| 255 | + throw new SDKException("too many attribute FQNs: " + fqns.size() |
| 256 | + + " exceeds maximum of " + MAX_VALIDATE_FQNS); |
| 257 | + } |
| 258 | + for (String fqn : fqns) { |
| 259 | + try { |
| 260 | + new Autoconfigure.AttributeValueFQN(fqn); |
| 261 | + } catch (AutoConfigureException e) { |
| 262 | + throw new SDKException("invalid attribute value FQN \"" + fqn + "\": " + e.getMessage(), e); |
| 263 | + } |
| 264 | + } |
| 265 | + GetAttributeValuesByFqnsResponse resp = ResponseMessageKt.getOrThrow( |
| 266 | + services.attributes() |
| 267 | + .getAttributeValuesByFqnsBlocking( |
| 268 | + GetAttributeValuesByFqnsRequest.newBuilder().addAllFqns(fqns).build(), |
| 269 | + Collections.emptyMap()) |
| 270 | + .execute()); |
| 271 | + Map<String, GetAttributeValuesByFqnsResponse.AttributeAndValue> found = resp.getFqnAttributeValuesMap(); |
| 272 | + List<String> missing = fqns.stream() |
| 273 | + .filter(f -> !found.containsKey(f)) |
| 274 | + .collect(Collectors.toList()); |
| 275 | + if (!missing.isEmpty()) { |
| 276 | + throw new AttributeNotFoundException("attribute not found: " + String.join(", ", missing)); |
| 277 | + } |
| 278 | + } |
| 279 | + |
| 280 | + /** |
| 281 | + * Returns the attribute value FQNs assigned to an entity (person or non-person entity). |
| 282 | + * |
| 283 | + * <p>Use this to inspect what attributes a user, service account, or other entity has been |
| 284 | + * granted before making authorization decisions or constructing access policies. |
| 285 | + * |
| 286 | + * @param entity the entity to look up; must not be null |
| 287 | + * @return list of attribute value FQNs assigned to the entity, or an empty list if none |
| 288 | + * @throws SDKException if entity is null or a service error occurs |
| 289 | + */ |
| 290 | + public List<String> getEntityAttributes(Entity entity) { |
| 291 | + if (entity == null) { |
| 292 | + throw new SDKException("entity must not be null"); |
| 293 | + } |
| 294 | + GetEntitlementsResponse resp = ResponseMessageKt.getOrThrow( |
| 295 | + services.authorization() |
| 296 | + .getEntitlementsBlocking( |
| 297 | + GetEntitlementsRequest.newBuilder().addEntities(entity).build(), |
| 298 | + Collections.emptyMap()) |
| 299 | + .execute()); |
| 300 | + String entityId = entity.getId(); |
| 301 | + for (EntityEntitlements e : resp.getEntitlementsList()) { |
| 302 | + if (e.getEntityId().equals(entityId)) { |
| 303 | + return e.getAttributeValueFqnsList(); |
| 304 | + } |
| 305 | + } |
| 306 | + return Collections.emptyList(); |
| 307 | + } |
| 308 | + |
| 309 | + /** |
| 310 | + * Reports whether the attribute definition identified by {@code attributeFqn} exists on the |
| 311 | + * platform. |
| 312 | + * |
| 313 | + * <p>{@code attributeFqn} should be an attribute-level FQN (no {@code /value/} segment): |
| 314 | + * <pre>{@code https://<namespace>/attr/<attribute_name>}</pre> |
| 315 | + * |
| 316 | + * @param attributeFqn the attribute-level FQN to check |
| 317 | + * @return {@code true} if the attribute exists, {@code false} if it does not |
| 318 | + * @throws SDKException if the FQN format is invalid or a non-not-found service error occurs |
| 319 | + */ |
| 320 | + public boolean attributeExists(String attributeFqn) { |
| 321 | + try { |
| 322 | + new Autoconfigure.AttributeNameFQN(attributeFqn); |
| 323 | + } catch (AutoConfigureException e) { |
| 324 | + throw new SDKException("invalid attribute FQN \"" + attributeFqn + "\": " + e.getMessage(), e); |
| 325 | + } |
| 326 | + try { |
| 327 | + ResponseMessageKt.getOrThrow( |
| 328 | + services.attributes() |
| 329 | + .getAttributeBlocking( |
| 330 | + GetAttributeRequest.newBuilder().setFqn(attributeFqn).build(), |
| 331 | + Collections.emptyMap()) |
| 332 | + .execute()); |
| 333 | + return true; |
| 334 | + } catch (Exception e) { |
| 335 | + String msg = e.getMessage(); |
| 336 | + if (msg != null && msg.contains("not_found")) { |
| 337 | + return false; |
| 338 | + } |
| 339 | + throw new SDKException("checking attribute existence: " + msg, e); |
| 340 | + } |
| 341 | + } |
| 342 | + |
| 343 | + /** |
| 344 | + * Reports whether the attribute value FQN exists on the platform. |
| 345 | + * |
| 346 | + * <p>{@code valueFqn} should be a full attribute value FQN (with {@code /value/} segment): |
| 347 | + * <pre>{@code https://<namespace>/attr/<attribute_name>/value/<value>}</pre> |
| 348 | + * |
| 349 | + * @param valueFqn the attribute value FQN to check |
| 350 | + * @return {@code true} if the value exists, {@code false} if it does not |
| 351 | + * @throws SDKException if the FQN format is invalid or a service error occurs |
| 352 | + */ |
| 353 | + public boolean attributeValueExists(String valueFqn) { |
| 354 | + try { |
| 355 | + new Autoconfigure.AttributeValueFQN(valueFqn); |
| 356 | + } catch (AutoConfigureException e) { |
| 357 | + throw new SDKException("invalid attribute value FQN \"" + valueFqn + "\": " + e.getMessage(), e); |
| 358 | + } |
| 359 | + GetAttributeValuesByFqnsResponse resp; |
| 360 | + try { |
| 361 | + resp = ResponseMessageKt.getOrThrow( |
| 362 | + services.attributes() |
| 363 | + .getAttributeValuesByFqnsBlocking( |
| 364 | + GetAttributeValuesByFqnsRequest.newBuilder().addFqns(valueFqn).build(), |
| 365 | + Collections.emptyMap()) |
| 366 | + .execute()); |
| 367 | + } catch (Exception e) { |
| 368 | + throw new SDKException("checking attribute value existence: " + e.getMessage(), e); |
| 369 | + } |
| 370 | + return resp.getFqnAttributeValuesMap().containsKey(valueFqn); |
| 371 | + } |
| 372 | + |
182 | 373 | /** |
183 | 374 | * Indicates that the TDF is malformed in some way |
184 | 375 | */ |
@@ -284,4 +475,16 @@ public AssertionException(String errorMessage, String id) { |
284 | 475 | super("assertion id: "+ id + "; " + errorMessage); |
285 | 476 | } |
286 | 477 | } |
| 478 | + |
| 479 | + /** |
| 480 | + * {@link AttributeNotFoundException} is thrown by {@link #validateAttributes(List)}, |
| 481 | + * {@link #validateAttributeExists(String)}, and |
| 482 | + * {@link #validateAttributeValue(String, String)} when one or more attributes or values |
| 483 | + * are not found on the platform. |
| 484 | + */ |
| 485 | + public static class AttributeNotFoundException extends SDKException { |
| 486 | + public AttributeNotFoundException(String errorMessage) { |
| 487 | + super(errorMessage); |
| 488 | + } |
| 489 | + } |
287 | 490 | } |
0 commit comments