diff --git a/Dockerfile b/Dockerfile index 45cd6c20..4a181c25 100644 --- a/Dockerfile +++ b/Dockerfile @@ -710,7 +710,9 @@ RUN --mount=type=secret,id=confidence_client_secret \ FROM openfeature-provider-java.test AS openfeature-provider-java.test_e2e RUN --mount=type=secret,id=confidence_client_secret \ + --mount=type=secret,id=confidence_client_encryption_key \ CONFIDENCE_CLIENT_SECRET=$(cat /run/secrets/confidence_client_secret) \ + CONFIDENCE_CLIENT_ENCRYPTION_KEY=$(cat /run/secrets/confidence_client_encryption_key) \ make test-e2e # ============================================================================== diff --git a/openfeature-provider/java/src/main/java/com/spotify/confidence/sdk/FlagsAdminStateFetcher.java b/openfeature-provider/java/src/main/java/com/spotify/confidence/sdk/FlagsAdminStateFetcher.java index ebaaa500..8cad2f6e 100644 --- a/openfeature-provider/java/src/main/java/com/spotify/confidence/sdk/FlagsAdminStateFetcher.java +++ b/openfeature-provider/java/src/main/java/com/spotify/confidence/sdk/FlagsAdminStateFetcher.java @@ -25,28 +25,22 @@ class FlagsAdminStateFetcher implements AccountStateProvider { "https://confidence-resolver-state-cdn.spotifycdn.com/"; private final String clientSecret; + private final String encryptionKey; private final HttpClientFactory httpClientFactory; - // ETag for conditional GETs of resolver state private final AtomicReference etagHolder = new AtomicReference<>(); - private final AtomicReference rawResolverStateHolder = - new AtomicReference<>( - com.spotify.confidence.sdk.flags.admin.v1.ResolverState.newBuilder() - .build() - .toByteArray()); + private final AtomicReference rawStateHolder = new AtomicReference<>(); private String accountId = ""; - public FlagsAdminStateFetcher(String clientSecret, HttpClientFactory httpClientFactory) { + public FlagsAdminStateFetcher( + String clientSecret, HttpClientFactory httpClientFactory, String encryptionKey) { this.clientSecret = clientSecret; this.httpClientFactory = httpClientFactory; - } - - public AtomicReference rawStateHolder() { - return rawResolverStateHolder; + this.encryptionKey = encryptionKey; } @Override public byte[] provide() { - return rawResolverStateHolder.get(); + return rawStateHolder.get(); } @Override @@ -63,9 +57,13 @@ public void reload() { } } + boolean isEncrypted() { + return encryptionKey != null; + } + private void fetchAndUpdateStateIfChanged() { - // Build CDN URL using SHA256 hash of client secret - final var cdnUrl = CDN_BASE_URL + sha256Hex(clientSecret); + final String hash = sha256Hex(clientSecret); + final var cdnUrl = CDN_BASE_URL + hash + (isEncrypted() ? ".enc" : ""); try { final HttpURLConnection conn = httpClientFactory.create(cdnUrl); final String previousEtag = etagHolder.get(); @@ -73,23 +71,23 @@ private void fetchAndUpdateStateIfChanged() { conn.setRequestProperty("if-none-match", previousEtag); } if (conn.getResponseCode() == 304) { - // Not modified return; } final String etag = conn.getHeaderField("etag"); try (final InputStream stream = conn.getInputStream()) { final byte[] bytes = stream.readAllBytes(); - // Parse SetResolverStateRequest from CDN response - final var stateRequest = - com.spotify.confidence.sdk.wasm.Messages.SetResolverStateRequest.parseFrom(bytes); - this.accountId = stateRequest.getAccountId(); - - // Store the state bytes (already in bytes format) - rawResolverStateHolder.set(stateRequest.getState().toByteArray()); + if (isEncrypted()) { + rawStateHolder.set(bytes); + } else { + final var stateRequest = + com.spotify.confidence.sdk.wasm.Messages.SetResolverStateRequest.parseFrom(bytes); + this.accountId = stateRequest.getAccountId(); + rawStateHolder.set(stateRequest.getState().toByteArray()); + } etagHolder.set(etag); } - logger.info("Loaded resolver state for account={}, etag={}", accountId, etag); + logger.info("Loaded resolver state (encrypted={}, etag={})", isEncrypted(), etag); } catch (IOException e) { throw new RuntimeException(e); } diff --git a/openfeature-provider/java/src/main/java/com/spotify/confidence/sdk/LocalProviderConfig.java b/openfeature-provider/java/src/main/java/com/spotify/confidence/sdk/LocalProviderConfig.java index 173a4b81..1c2b9c4a 100644 --- a/openfeature-provider/java/src/main/java/com/spotify/confidence/sdk/LocalProviderConfig.java +++ b/openfeature-provider/java/src/main/java/com/spotify/confidence/sdk/LocalProviderConfig.java @@ -11,6 +11,7 @@ public class LocalProviderConfig { private final HttpClientFactory httpClientFactory; private final boolean useRemoteMaterializationStore; private final int resolverPoolSize; + private final String encryptionKey; public LocalProviderConfig() { this(null, null); @@ -36,11 +37,21 @@ public LocalProviderConfig( HttpClientFactory httpClientFactory, boolean useRemoteMaterializationStore, int resolverPoolSize) { + this(channelFactory, httpClientFactory, useRemoteMaterializationStore, resolverPoolSize, null); + } + + private LocalProviderConfig( + ChannelFactory channelFactory, + HttpClientFactory httpClientFactory, + boolean useRemoteMaterializationStore, + int resolverPoolSize, + String encryptionKey) { this.channelFactory = channelFactory != null ? channelFactory : new DefaultChannelFactory(); this.httpClientFactory = httpClientFactory != null ? httpClientFactory : new DefaultHttpClientFactory(); this.useRemoteMaterializationStore = useRemoteMaterializationStore; this.resolverPoolSize = resolverPoolSize > 0 ? resolverPoolSize : DEFAULT_RESOLVER_POOL_SIZE; + this.encryptionKey = encryptionKey; } public ChannelFactory getChannelFactory() { @@ -63,6 +74,11 @@ public int getResolverPoolSize() { return resolverPoolSize; } + /** Returns the hex-encoded AES-256 encryption key, or {@code null} if unset. */ + public String getEncryptionKey() { + return encryptionKey; + } + public static Builder builder() { return new Builder(); } @@ -72,6 +88,7 @@ public static class Builder { private HttpClientFactory httpClientFactory; private boolean useRemoteMaterializationStore; private int resolverPoolSize; + private String encryptionKey; public Builder channelFactory(ChannelFactory channelFactory) { this.channelFactory = channelFactory; @@ -100,9 +117,19 @@ public Builder resolverPoolSize(int resolverPoolSize) { return this; } + /** Sets the hex-encoded AES-256 encryption key for decrypting CDN state. */ + public Builder encryptionKey(String encryptionKey) { + this.encryptionKey = encryptionKey; + return this; + } + public LocalProviderConfig build() { return new LocalProviderConfig( - channelFactory, httpClientFactory, useRemoteMaterializationStore, resolverPoolSize); + channelFactory, + httpClientFactory, + useRemoteMaterializationStore, + resolverPoolSize, + encryptionKey); } } } diff --git a/openfeature-provider/java/src/main/java/com/spotify/confidence/sdk/LocalResolver.java b/openfeature-provider/java/src/main/java/com/spotify/confidence/sdk/LocalResolver.java index e0c3e1da..f908b297 100644 --- a/openfeature-provider/java/src/main/java/com/spotify/confidence/sdk/LocalResolver.java +++ b/openfeature-provider/java/src/main/java/com/spotify/confidence/sdk/LocalResolver.java @@ -19,6 +19,8 @@ interface LocalResolver { */ void setResolverState(byte[] state, String accountId, Sdk sdk); + void setEncryptedResolverState(byte[] encryptedState, byte[] encryptionKey, Sdk sdk); + /** * Resolves flags. The returned stage completes when all resolution (including any store I/O for * materializations) has finished. diff --git a/openfeature-provider/java/src/main/java/com/spotify/confidence/sdk/MaterializingResolver.java b/openfeature-provider/java/src/main/java/com/spotify/confidence/sdk/MaterializingResolver.java index 5f1753c6..c4580888 100644 --- a/openfeature-provider/java/src/main/java/com/spotify/confidence/sdk/MaterializingResolver.java +++ b/openfeature-provider/java/src/main/java/com/spotify/confidence/sdk/MaterializingResolver.java @@ -167,6 +167,11 @@ public void setResolverState(byte[] state, String accountId, Sdk sdk) { delegate.setResolverState(state, accountId, sdk); } + @Override + public void setEncryptedResolverState(byte[] encryptedState, byte[] encryptionKey, Sdk sdk) { + delegate.setEncryptedResolverState(encryptedState, encryptionKey, sdk); + } + @Override public void registerResolve(RegisterResolveRequest request) { delegate.registerResolve(request); diff --git a/openfeature-provider/java/src/main/java/com/spotify/confidence/sdk/OpenFeatureLocalResolveProvider.java b/openfeature-provider/java/src/main/java/com/spotify/confidence/sdk/OpenFeatureLocalResolveProvider.java index 926e5d0f..6209cbc4 100644 --- a/openfeature-provider/java/src/main/java/com/spotify/confidence/sdk/OpenFeatureLocalResolveProvider.java +++ b/openfeature-provider/java/src/main/java/com/spotify/confidence/sdk/OpenFeatureLocalResolveProvider.java @@ -64,6 +64,7 @@ public class OpenFeatureLocalResolveProvider implements FeatureProvider { private final AccountStateProvider stateProvider; private final AtomicReference state = new AtomicReference<>(ProviderState.NOT_READY); + private final String encryptionKey; private volatile boolean initialized = false; private volatile byte[] lastStateBytes = null; @VisibleForTesting boolean forcedFetcherShutdown = false; @@ -147,8 +148,11 @@ public OpenFeatureLocalResolveProvider( public OpenFeatureLocalResolveProvider( LocalProviderConfig config, String clientSecret, MaterializationStore materializationStore) { this.clientSecret = clientSecret; + this.encryptionKey = config.getEncryptionKey(); this.materializationStore = materializationStore; - this.stateProvider = new FlagsAdminStateFetcher(clientSecret, config.getHttpClientFactory()); + this.stateProvider = + new FlagsAdminStateFetcher( + clientSecret, config.getHttpClientFactory(), config.getEncryptionKey()); final var wasmFlagLogger = new GrpcWasmFlagLogger(clientSecret, config.getChannelFactory()); this.flagLogger = wasmFlagLogger; final int numInstances = PooledResolver.getNumInstances(config.getResolverPoolSize()); @@ -174,6 +178,7 @@ public OpenFeatureLocalResolveProvider( MaterializationStore materializationStore, WasmFlagLogger wasmFlagLogger) { this.clientSecret = clientSecret; + this.encryptionKey = null; this.materializationStore = materializationStore; this.stateProvider = accountStateProvider; this.flagLogger = wasmFlagLogger; @@ -193,14 +198,13 @@ public ProviderState getState() { @Override public void initialize(EvaluationContext evaluationContext) { + if (encryptionKey == null) { + log.warn( + "No encryptionKey provided. Falling back to unencrypted state." + + " An encryption key will be required in an upcoming version."); + } stateProvider.reload(); - final AtomicReference resolverStateProtobuf = - new AtomicReference<>(stateProvider.provide()); - final AtomicReference accountIdRef = new AtomicReference<>(stateProvider.accountId()); - - // Only initialize WASM and set READY if we got valid state (non-empty accountId) - if (!accountIdRef.get().isEmpty()) { - resolver.setResolverState(resolverStateProtobuf.get(), accountIdRef.get(), SDK); + if (pushStateToResolver()) { initialized = true; this.state.set(ProviderState.READY); } else { @@ -210,7 +214,7 @@ public void initialize(EvaluationContext evaluationContext) { } final long pollIntervalSeconds = getPollIntervalSeconds(); - scheduleStateRefresh(resolverStateProtobuf, accountIdRef, pollIntervalSeconds); + scheduleStateRefresh(pollIntervalSeconds); assignLogExecutor.scheduleAtFixedRate( () -> { @@ -227,53 +231,62 @@ public void initialize(EvaluationContext evaluationContext) { TimeUnit.MILLISECONDS); } - private void scheduleStateRefresh( - AtomicReference resolverStateProtobuf, - AtomicReference accountIdRef, - long pollIntervalSeconds) { + private boolean pushStateToResolver() { + final byte[] stateBytes = stateProvider.provide(); + if (stateBytes == null || stateBytes.length == 0) { + return false; + } + if (encryptionKey != null) { + resolver.setEncryptedResolverState(stateBytes, hexToBytes(encryptionKey), SDK); + } else { + final String accountId = stateProvider.accountId(); + if (accountId == null || accountId.isEmpty()) { + return false; + } + resolver.setResolverState(stateBytes, accountId, SDK); + } + lastStateBytes = stateBytes; + return true; + } + + private void scheduleStateRefresh(long pollIntervalSeconds) { if (flagsFetcherExecutor.isShutdown()) { return; } - // Use short retry interval (1s) when not initialized, normal interval otherwise long delaySeconds = initialized ? pollIntervalSeconds : 1; flagsFetcherExecutor.schedule( () -> { try { stateProvider.reload(); - resolverStateProtobuf.set(stateProvider.provide()); - accountIdRef.set(stateProvider.accountId()); - if (!accountIdRef.get().isEmpty()) { + final byte[] newState = stateProvider.provide(); + if (newState != null && !java.util.Arrays.equals(newState, lastStateBytes)) { + pushStateToResolver(); if (!initialized) { - resolver.setResolverState(resolverStateProtobuf.get(), accountIdRef.get(), SDK); - lastStateBytes = resolverStateProtobuf.get(); initialized = true; this.state.set(ProviderState.READY); log.info("Provider recovered and is now READY"); - } else { - // Only push state into the wasm instances when it actually changed — the wasm - // execution inside setResolverState is expensive (runs across all pool slots). - final byte[] newState = resolverStateProtobuf.get(); - if (!java.util.Arrays.equals(newState, lastStateBytes)) { - resolver.setResolverState(newState, accountIdRef.get(), SDK); - lastStateBytes = newState; - } - // Always flush logs regardless of state change. - resolver.flushAllLogs(); } } + if (initialized) { + resolver.flushAllLogs(); + } } catch (RuntimeException e) { log.error("State refresh failed", e); } finally { - scheduleStateRefresh(resolverStateProtobuf, accountIdRef, pollIntervalSeconds); + scheduleStateRefresh(pollIntervalSeconds); } }, delaySeconds, TimeUnit.SECONDS); } + private static byte[] hexToBytes(String hex) { + return java.util.HexFormat.of().parseHex(hex); + } + @Override public Metadata getMetadata() { return () -> "confidence-sdk-java-local"; diff --git a/openfeature-provider/java/src/main/java/com/spotify/confidence/sdk/PooledResolver.java b/openfeature-provider/java/src/main/java/com/spotify/confidence/sdk/PooledResolver.java index 137f49fa..b6795545 100644 --- a/openfeature-provider/java/src/main/java/com/spotify/confidence/sdk/PooledResolver.java +++ b/openfeature-provider/java/src/main/java/com/spotify/confidence/sdk/PooledResolver.java @@ -72,6 +72,11 @@ public void setResolverState(byte[] state, String accountId, Sdk sdk) { maintenance(lr -> lr.setResolverState(state, accountId, sdk)); } + @Override + public void setEncryptedResolverState(byte[] encryptedState, byte[] encryptionKey, Sdk sdk) { + maintenance(lr -> lr.setEncryptedResolverState(encryptedState, encryptionKey, sdk)); + } + @Override public void flushAllLogs() { maintenance(LocalResolver::flushAllLogs); diff --git a/openfeature-provider/java/src/main/java/com/spotify/confidence/sdk/RecoveringResolver.java b/openfeature-provider/java/src/main/java/com/spotify/confidence/sdk/RecoveringResolver.java index 2dd058dd..c4586a17 100644 --- a/openfeature-provider/java/src/main/java/com/spotify/confidence/sdk/RecoveringResolver.java +++ b/openfeature-provider/java/src/main/java/com/spotify/confidence/sdk/RecoveringResolver.java @@ -22,12 +22,10 @@ class RecoveringResolver implements LocalResolver { private static final Logger logger = LoggerFactory.getLogger(RecoveringResolver.class); - private record StateRecord(byte[] state, String accountId, Sdk sdk) {} - private final Supplier factory; private final AtomicReference current = new AtomicReference<>(); private final AtomicBoolean broken = new AtomicBoolean(false); - private final AtomicReference lastState = new AtomicReference<>(); + private volatile java.util.function.Consumer replayState; RecoveringResolver(Supplier factory) { this.factory = factory; @@ -41,9 +39,8 @@ private void startRecreate() { try { final LocalResolver old = current.get(); final LocalResolver newResolver = factory.get(); - final StateRecord cached = lastState.get(); - if (cached != null) { - newResolver.setResolverState(cached.state(), cached.accountId(), cached.sdk()); + if (replayState != null) { + replayState.accept(newResolver); } current.set(newResolver); if (old != null) { @@ -83,13 +80,24 @@ private void handleFailure(String opName, ChicoryException e) { public void setResolverState(byte[] state, String accountId, Sdk sdk) { try { current.get().setResolverState(state, accountId, sdk); - lastState.set(new StateRecord(state, accountId, sdk)); + replayState = lr -> lr.setResolverState(state, accountId, sdk); } catch (ChicoryException e) { handleFailure("setResolverState", e); throw e; } } + @Override + public void setEncryptedResolverState(byte[] encryptedState, byte[] encryptionKey, Sdk sdk) { + try { + current.get().setEncryptedResolverState(encryptedState, encryptionKey, sdk); + replayState = lr -> lr.setEncryptedResolverState(encryptedState, encryptionKey, sdk); + } catch (ChicoryException e) { + handleFailure("setEncryptedResolverState", e); + throw e; + } + } + @Override public CompletionStage resolveProcess(ResolveProcessRequest request) { try { diff --git a/openfeature-provider/java/src/main/java/com/spotify/confidence/sdk/WasmLocalResolver.java b/openfeature-provider/java/src/main/java/com/spotify/confidence/sdk/WasmLocalResolver.java index 17c8bd1b..790c1c36 100644 --- a/openfeature-provider/java/src/main/java/com/spotify/confidence/sdk/WasmLocalResolver.java +++ b/openfeature-provider/java/src/main/java/com/spotify/confidence/sdk/WasmLocalResolver.java @@ -52,6 +52,7 @@ class WasmLocalResolver implements LocalResolver { // api private final ExportFunction wasmMsgGuestSetResolverState; + private final ExportFunction wasmMsgGuestSetEncryptedResolverState; private final ExportFunction wasmMsgGuestRegisterResolve; private final ExportFunction wasmMsgBoundedFlushLogs; private final ExportFunction wasmMsgBoundedFlushAssign; @@ -84,6 +85,8 @@ public WasmLocalResolver(Consumer logSink) { wasmMsgAlloc = instance.export("wasm_msg_alloc"); wasmMsgFree = instance.export("wasm_msg_free"); wasmMsgGuestSetResolverState = instance.export("wasm_msg_guest_set_resolver_state"); + wasmMsgGuestSetEncryptedResolverState = + instance.export("wasm_msg_guest_set_encrypted_resolver_state"); wasmMsgGuestRegisterResolve = instance.export("wasm_msg_guest_register_resolve"); wasmMsgBoundedFlushLogs = instance.export("wasm_msg_guest_bounded_flush_logs"); wasmMsgBoundedFlushAssign = instance.export("wasm_msg_guest_bounded_flush_assign"); @@ -131,6 +134,30 @@ public void setResolverState(byte[] state, String accountId, Sdk sdk) { } } + @Override + public void setEncryptedResolverState(byte[] encryptedState, byte[] encryptionKey, Sdk sdk) { + lock.lock(); + try { + final var builder = + Messages.SetEncryptedResolverStateRequest.newBuilder() + .setEncryptedState(ByteString.copyFrom(encryptedState)) + .setEncryptionKey(ByteString.copyFrom(encryptionKey)); + if (sdk != null) { + builder.setSdk(sdk); + } + final byte[] request = + Messages.Request.newBuilder() + .setData(ByteString.copyFrom(builder.build().toByteArray())) + .build() + .toByteArray(); + final int addr = transfer(request); + final int respPtr = (int) wasmMsgGuestSetEncryptedResolverState.apply(addr)[0]; + consumeResponse(respPtr, Messages.Void::parseFrom); + } finally { + lock.unlock(); + } + } + @Override public CompletionStage resolveProcess(ResolveProcessRequest request) { lock.lock(); diff --git a/openfeature-provider/java/src/test/java/com/spotify/confidence/sdk/EncryptedStateTest.java b/openfeature-provider/java/src/test/java/com/spotify/confidence/sdk/EncryptedStateTest.java new file mode 100644 index 00000000..819aedce --- /dev/null +++ b/openfeature-provider/java/src/test/java/com/spotify/confidence/sdk/EncryptedStateTest.java @@ -0,0 +1,79 @@ +package com.spotify.confidence.sdk; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import com.google.protobuf.util.Structs; +import com.google.protobuf.util.Values; +import com.spotify.confidence.sdk.flags.resolver.v1.ResolveFlagsRequest; +import com.spotify.confidence.sdk.flags.resolver.v1.ResolveProcessRequest; +import com.spotify.confidence.sdk.flags.resolver.v1.ResolveProcessResponse; +import com.spotify.confidence.sdk.flags.resolver.v1.ResolveReason; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.HexFormat; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class EncryptedStateTest { + + private static final String CLIENT_SECRET = "mkjJruAATQWjeY7foFIWfVAcBWnci2YF"; + + private WasmLocalResolver resolver; + private byte[] encryptedState; + private byte[] encryptionKey; + + @BeforeEach + void setUp() throws IOException { + resolver = new WasmLocalResolver(request -> {}); + encryptedState = getClass().getResourceAsStream("/resolver_state_encrypted.pb").readAllBytes(); + encryptionKey = + HexFormat.of() + .parseHex( + new String( + getClass().getResourceAsStream("/encryption_key_test.hex").readAllBytes(), + StandardCharsets.UTF_8) + .trim()); + } + + @Test + void shouldResolveAfterSettingEncryptedState() { + resolver.setEncryptedResolverState(encryptedState, encryptionKey, null); + + final var request = + ResolveProcessRequest.newBuilder() + .setDeferredMaterializations( + ResolveFlagsRequest.newBuilder() + .addAllFlags(List.of("flags/tutorial-feature")) + .setClientSecret(CLIENT_SECRET) + .setEvaluationContext( + Structs.of( + "targeting_key", + Values.of("tutorial_visitor"), + "visitor_id", + Values.of("tutorial_visitor"))) + .setApply(true) + .build()) + .build(); + + final ResolveProcessResponse response = + resolver.resolveProcess(request).toCompletableFuture().join(); + assertThat(response.hasResolved()).isTrue(); + assertThat(response.getResolved().getResponse().getResolvedFlagsCount()).isEqualTo(1); + final var resolvedFlag = response.getResolved().getResponse().getResolvedFlags(0); + assertThat(resolvedFlag.getReason()) + .withFailMessage( + "Expected MATCH but got %s (flag=%s, variant=%s)", + resolvedFlag.getReason(), resolvedFlag.getFlag(), resolvedFlag.getVariant()) + .isEqualTo(ResolveReason.RESOLVE_REASON_MATCH); + } + + @Test + void shouldRejectWrongEncryptionKey() { + final byte[] wrongKey = new byte[32]; + assertThrows( + RuntimeException.class, + () -> resolver.setEncryptedResolverState(encryptedState, wrongKey, null)); + } +} diff --git a/openfeature-provider/java/src/test/java/com/spotify/confidence/sdk/OpenFeatureLocalResolveProviderEncryptedIT.java b/openfeature-provider/java/src/test/java/com/spotify/confidence/sdk/OpenFeatureLocalResolveProviderEncryptedIT.java new file mode 100644 index 00000000..31172846 --- /dev/null +++ b/openfeature-provider/java/src/test/java/com/spotify/confidence/sdk/OpenFeatureLocalResolveProviderEncryptedIT.java @@ -0,0 +1,47 @@ +package com.spotify.confidence.sdk; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; + +import dev.openfeature.sdk.*; +import org.junit.jupiter.api.*; + +class OpenFeatureLocalResolveProviderEncryptedIT { + private static final String FLAG_CLIENT_SECRET = System.getenv("CONFIDENCE_CLIENT_SECRET"); + private static final String ENCRYPTION_KEY = System.getenv("CONFIDENCE_CLIENT_ENCRYPTION_KEY"); + private static Client client; + + @BeforeAll + static void setup() { + final var provider = + new OpenFeatureLocalResolveProvider( + LocalProviderConfig.builder().encryptionKey(ENCRYPTION_KEY).build(), + FLAG_CLIENT_SECRET); + OpenFeatureAPI.getInstance().setProviderAndWait("encrypted-e2e", provider); + final EvaluationContext context = new MutableContext("test-a").add("sticky", false); + OpenFeatureAPI.getInstance().setEvaluationContext(context); + client = OpenFeatureAPI.getInstance().getClient("encrypted-e2e"); + } + + @AfterAll + static void teardown() { + OpenFeatureAPI.getInstance().shutdown(); + } + + @Test + void shouldResolveBooleanViaEncryptedState() { + assertThat(client.getBooleanValue("web-sdk-e2e-flag.bool", true)).isFalse(); + } + + @Test + void shouldResolveStringViaEncryptedState() { + assertThat(client.getStringValue("web-sdk-e2e-flag.str", "default")).isEqualTo("control"); + } + + @Test + void shouldResolveDetailsViaEncryptedState() { + final FlagEvaluationDetails details = + client.getDoubleDetails("web-sdk-e2e-flag.obj.double", 1.0); + assertThat(details.getValue()).isEqualTo(3.6); + assertThat(details.getReason()).isEqualTo("RESOLVE_REASON_MATCH"); + } +} diff --git a/openfeature-provider/java/src/test/java/com/spotify/confidence/sdk/OpenFeatureLocalResolveProviderFlagLogsTest.java b/openfeature-provider/java/src/test/java/com/spotify/confidence/sdk/OpenFeatureLocalResolveProviderFlagLogsTest.java index dd19cd75..70a1e354 100644 --- a/openfeature-provider/java/src/test/java/com/spotify/confidence/sdk/OpenFeatureLocalResolveProviderFlagLogsTest.java +++ b/openfeature-provider/java/src/test/java/com/spotify/confidence/sdk/OpenFeatureLocalResolveProviderFlagLogsTest.java @@ -44,7 +44,7 @@ void setup() { // Create a state provider that fetches from the real Confidence service final var stateProvider = - new FlagsAdminStateFetcher(FLAG_CLIENT_SECRET, new DefaultHttpClientFactory()); + new FlagsAdminStateFetcher(FLAG_CLIENT_SECRET, new DefaultHttpClientFactory(), null); stateProvider.reload(); // Create provider with capturing logger diff --git a/openfeature-provider/java/src/test/java/com/spotify/confidence/sdk/WasmResolveApiFlushCloseRaceTest.java b/openfeature-provider/java/src/test/java/com/spotify/confidence/sdk/WasmResolveApiFlushCloseRaceTest.java index dea5ff64..cdd63258 100644 --- a/openfeature-provider/java/src/test/java/com/spotify/confidence/sdk/WasmResolveApiFlushCloseRaceTest.java +++ b/openfeature-provider/java/src/test/java/com/spotify/confidence/sdk/WasmResolveApiFlushCloseRaceTest.java @@ -31,7 +31,7 @@ class WasmResolveApiFlushCloseRaceTest { @BeforeAll static void fetchState() { final var stateProvider = - new FlagsAdminStateFetcher(FLAG_CLIENT_SECRET, new DefaultHttpClientFactory()); + new FlagsAdminStateFetcher(FLAG_CLIENT_SECRET, new DefaultHttpClientFactory(), null); stateProvider.reload(); resolverState = stateProvider.provide(); accountId = stateProvider.accountId(); diff --git a/openfeature-provider/java/src/test/resources/encryption_key_test.hex b/openfeature-provider/java/src/test/resources/encryption_key_test.hex new file mode 100644 index 00000000..28373acb --- /dev/null +++ b/openfeature-provider/java/src/test/resources/encryption_key_test.hex @@ -0,0 +1 @@ +342b666ceb14fb94ed1d7da0a85acb529ca2a244c3193c6b02251044a454f4a2 \ No newline at end of file diff --git a/openfeature-provider/java/src/test/resources/resolver_state_encrypted.pb b/openfeature-provider/java/src/test/resources/resolver_state_encrypted.pb new file mode 100644 index 00000000..8b9b1dcc Binary files /dev/null and b/openfeature-provider/java/src/test/resources/resolver_state_encrypted.pb differ