Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package io.opentdf.platform.sdk;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

// Mirrors the KASKeyCache pattern: null returns, LocalDateTime.now(), and 5-minute TTL.
class KASAllowlistCache {
private static final Logger log = LoggerFactory.getLogger(KASAllowlistCache.class);
final Map<String, TimeStampedAllowList> cache = new ConcurrentHashMap<>();

public void clear() {
cache.clear();
}

public Set<String> get(String platformURL) {
log.debug("retrieving allowlist for platformURL = [{}]", platformURL);
TimeStampedAllowList cachedValue = cache.get(platformURL);
if (cachedValue == null) {
log.debug("didn't find allowlist for platformURL = [{}]", platformURL);
return null;

Check warning on line 28 in sdk/src/main/java/io/opentdf/platform/sdk/KASAllowlistCache.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Return an empty collection instead of null.

See more on https://sonarcloud.io/project/issues?id=opentdf_java-sdk&issues=AaAfvHkA0CwIYMuUgBMo&open=AaAfvHkA0CwIYMuUgBMo&pullRequest=391
}

LocalDateTime fiveMinAgo = LocalDateTime.now().minus(5, ChronoUnit.MINUTES);

Check warning on line 31 in sdk/src/main/java/io/opentdf/platform/sdk/KASAllowlistCache.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Explicitly specify the time zone by passing a ZoneId or a Clock to the .now() method.

See more on https://sonarcloud.io/project/issues?id=opentdf_java-sdk&issues=AaAfvHkA0CwIYMuUgBMp&open=AaAfvHkA0CwIYMuUgBMp&pullRequest=391
if (fiveMinAgo.isAfter(cachedValue.timestamp)) {
log.debug("cached allowlist is too old timestamp = [{}] for platformURL = [{}]",
cachedValue.timestamp, platformURL);
cache.remove(platformURL);
return null;

Check warning on line 36 in sdk/src/main/java/io/opentdf/platform/sdk/KASAllowlistCache.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Return an empty collection instead of null.

See more on https://sonarcloud.io/project/issues?id=opentdf_java-sdk&issues=AaAfvHkA0CwIYMuUgBMq&open=AaAfvHkA0CwIYMuUgBMq&pullRequest=391
}

log.debug("successfully returned allowlist for platformURL = [{}]", platformURL);
return new HashSet<>(cachedValue.allowlist);
}

public void store(String platformURL, Set<String> allowlist) {
log.debug("storing allowlist into the cache for platformURL = [{}]", platformURL);
cache.put(platformURL, new TimeStampedAllowList(
Collections.unmodifiableSet(new HashSet<>(allowlist)), LocalDateTime.now()));

Check warning on line 46 in sdk/src/main/java/io/opentdf/platform/sdk/KASAllowlistCache.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Explicitly specify the time zone by passing a ZoneId or a Clock to the .now() method.

See more on https://sonarcloud.io/project/issues?id=opentdf_java-sdk&issues=AaAfyDYDDe4tBAqPPbw5&open=AaAfyDYDDe4tBAqPPbw5&pullRequest=391
}
}

class TimeStampedAllowList {
Set<String> allowlist;
LocalDateTime timestamp;

public TimeStampedAllowList(Set<String> allowlist, LocalDateTime timestamp) {
this.allowlist = allowlist;
this.timestamp = timestamp;
}
}
37 changes: 37 additions & 0 deletions sdk/src/main/java/io/opentdf/platform/sdk/SDK.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
import io.opentdf.platform.policy.attributes.GetAttributeValuesByFqnsResponse;
import io.opentdf.platform.policy.attributes.ListAttributesRequest;
import io.opentdf.platform.policy.kasregistry.KeyAccessServerRegistryServiceClientInterface;
import io.opentdf.platform.policy.kasregistry.ListKeyAccessServersRequest;
import io.opentdf.platform.policy.kasregistry.ListKeyAccessServersResponse;
import io.opentdf.platform.policy.namespaces.NamespaceServiceClientInterface;
import io.opentdf.platform.policy.resourcemapping.ResourceMappingServiceClientInterface;
import io.opentdf.platform.policy.subjectmapping.SubjectMappingServiceClientInterface;
Expand All @@ -30,9 +32,11 @@
import java.nio.channels.SeekableByteChannel;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;

Check warning on line 39 in sdk/src/main/java/io/opentdf/platform/sdk/SDK.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this unused import 'java.util.Set'.

See more on https://sonarcloud.io/project/issues?id=opentdf_java-sdk&issues=AaAfvHlU0CwIYMuUgBMs&open=AaAfvHlU0CwIYMuUgBMs&pullRequest=391
import java.util.stream.Collectors;

/**
Expand All @@ -57,6 +61,7 @@
private final String platformUrl;
private final ProtocolClient platformServicesClient;
private final SrtSigner srtSigner;
private final KASAllowlistCache kasAllowlistCache = new KASAllowlistCache();

/**
* Closes the SDK, including its associated services.
Expand Down Expand Up @@ -136,10 +141,42 @@
}

public TDF.Reader loadTDF(SeekableByteChannel channel, Config.TDFReaderConfig config) throws SDKException, IOException {
resolveKasAllowlist(config);
var tdf = new TDF(services);
return tdf.loadTDF(channel, config, platformUrl);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

private void resolveKasAllowlist(Config.TDFReaderConfig config) throws SDKException {
if (config.ignoreKasAllowlist
|| (config.kasAllowlist != null && !config.kasAllowlist.isEmpty())) {
return;
}

var cached = kasAllowlistCache.get(platformUrl);
if (cached != null) {
config.kasAllowlist = cached;
return;
}

var request = ListKeyAccessServersRequest.newBuilder().build();
ListKeyAccessServersResponse response;
try {
response = RequestHelper.getOrThrow(
services.kasRegistry().listKeyAccessServersBlocking(request, Collections.emptyMap()).execute());
} catch (com.connectrpc.ConnectException e) {
throw new SDKException("error getting kas servers", e);
}

var allowlist = new HashSet<String>();
for (var entry : response.getKeyAccessServersList()) {
allowlist.add(Config.getKasAddress(entry.getUri()));
}
allowlist.add(Config.getKasAddress(platformUrl));

config.kasAllowlist = allowlist;
kasAllowlistCache.store(platformUrl, allowlist);
}

public Manifest createTDF(InputStream payload, OutputStream outputStream, Config.TDFConfig config) throws SDKException, IOException {
var tdf = new TDF(services);
return tdf.createTDF(payload, outputStream, config).getManifest();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package io.opentdf.platform.sdk;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.Set;

import static org.junit.jupiter.api.Assertions.*;

class KASAllowlistCacheTest {

private KASAllowlistCache cache;

@BeforeEach
void setUp() {
cache = new KASAllowlistCache();
}

@Test
void testStoreAndGet_WithinTimeLimit() {
Set<String> allowlist = Set.of("https://kas1.example.org", "https://kas2.example.org");
cache.store("https://platform.example.org", allowlist);

Set<String> result = cache.get("https://platform.example.org");

assertNotNull(result);
assertEquals(2, result.size());
assertTrue(result.contains("https://kas1.example.org"));
assertTrue(result.contains("https://kas2.example.org"));
}

@Test
void testStoreAndGet_AfterTimeLimit() {
Set<String> allowlist = Set.of("https://kas.example.org");
cache.store("https://platform.example.org", allowlist);

TimeStampedAllowList expired = new TimeStampedAllowList(allowlist, LocalDateTime.now().minus(6, ChronoUnit.MINUTES));
cache.cache.put("https://platform.example.org", expired);

Set<String> result = cache.get("https://platform.example.org");

assertNull(result);
}

@Test
void testGet_EmptyCache() {
Set<String> result = cache.get("https://platform.example.org");
assertNull(result);
}

@Test
void testGet_DifferentKey() {
Set<String> allowlist = Set.of("https://kas.example.org");
cache.store("https://platform.example.org", allowlist);

Set<String> result = cache.get("https://other.example.org");

assertNull(result);
}

@Test
void testClearCache() {
Set<String> allowlist = Set.of("https://kas.example.org");
cache.store("https://platform.example.org", allowlist);

cache.clear();

Set<String> result = cache.get("https://platform.example.org");
assertNull(result);
}

@Test
void testStoreMultipleAndGet() {
Set<String> allowlist1 = Set.of("https://kas1.example.org");
Set<String> allowlist2 = Set.of("https://kas2.example.org");
cache.store("https://platform1.example.org", allowlist1);
cache.store("https://platform2.example.org", allowlist2);

Set<String> result1 = cache.get("https://platform1.example.org");
Set<String> result2 = cache.get("https://platform2.example.org");

assertNotNull(result1);
assertTrue(result1.contains("https://kas1.example.org"));
assertNotNull(result2);
assertTrue(result2.contains("https://kas2.example.org"));
}
}
Loading