diff --git a/lib/java-server-sdk-redis-store/README.md b/lib/java-server-sdk-redis-store/README.md index d2d2820..26204f3 100644 --- a/lib/java-server-sdk-redis-store/README.md +++ b/lib/java-server-sdk-redis-store/README.md @@ -23,10 +23,10 @@ This assumes that you have already installed the LaunchDarkly Java SDK. redis.clients jedis - 2.9.0 + 7.1.0 - This library is compatible with Jedis 2.x versions greater than or equal to 2.9.0, and also with Jedis 3.x. + This library uses Jedis 7.1.0 by default and requires Jedis 3.6.0 or later for username/password (ACL) authentication support. 3. Import the LaunchDarkly package and the package for this library: @@ -45,6 +45,44 @@ This assumes that you have already installed the LaunchDarkly Java SDK. By default, the store will try to connect to a local Redis instance on port 6379. +## Authentication + +### Password-only authentication (legacy) + +For Redis servers using simple password authentication: + + LDConfig config = new LDConfig.Builder() + .dataStore( + Components.persistentDataStore( + Redis.dataStore().password("my-redis-password") + ) + ) + .build(); + +Or include it in the URI: + + Redis.dataStore().url("redis://:my-redis-password@my-redis-host:6379") + +### Username/password authentication (Redis 6.0+ ACL) + +For Redis 6.0+ servers using ACL with username and password: + + LDConfig config = new LDConfig.Builder() + .dataStore( + Components.persistentDataStore( + Redis.dataStore() + .username("my-username") + .password("my-password") + ) + ) + .build(); + +Or include both in the URI: + + Redis.dataStore().url("redis://my-username:my-password@my-redis-host:6379") + +**Note:** Username/password authentication requires Jedis 3.6.0 or later. + ## Caching behavior The LaunchDarkly SDK has a standard caching mechanism for any persistent data store, to reduce database traffic. This is configured through the SDK's `PersistentDataStoreBuilder` class as described the SDK documentation. For instance, to specify a cache TTL of 5 minutes: diff --git a/lib/java-server-sdk-redis-store/build.gradle b/lib/java-server-sdk-redis-store/build.gradle index 1d8e62c..5d1eaa2 100644 --- a/lib/java-server-sdk-redis-store/build.gradle +++ b/lib/java-server-sdk-redis-store/build.gradle @@ -42,7 +42,7 @@ ext { ext.versions = [ "sdk": "6.3.0", // the *lowest* version we're compatible with - "jedis": "2.9.0" + "jedis": "7.1.0" // 3.6.0+ required for username/password (ACL) authentication ] ext.libraries = [:] diff --git a/lib/java-server-sdk-redis-store/src/main/java/com/launchdarkly/sdk/server/integrations/RedisStoreBuilder.java b/lib/java-server-sdk-redis-store/src/main/java/com/launchdarkly/sdk/server/integrations/RedisStoreBuilder.java index d8df2ec..1a7dbff 100644 --- a/lib/java-server-sdk-redis-store/src/main/java/com/launchdarkly/sdk/server/integrations/RedisStoreBuilder.java +++ b/lib/java-server-sdk-redis-store/src/main/java/com/launchdarkly/sdk/server/integrations/RedisStoreBuilder.java @@ -76,6 +76,7 @@ public abstract class RedisStoreBuilder implements ComponentConfigurer, Di Duration connectTimeout = Duration.ofMillis(Protocol.DEFAULT_TIMEOUT); Duration socketTimeout = Duration.ofMillis(Protocol.DEFAULT_TIMEOUT); Integer database = null; + String username = null; String password = null; boolean tls = false; JedisPoolConfig poolConfig = null; @@ -98,11 +99,30 @@ public RedisStoreBuilder database(Integer database) { return this; } + /** + * Specifies a username for Redis ACL authentication. + *

+ * Redis 6.0+ supports Access Control Lists (ACL) with username/password authentication. + * It is also possible to include a username in the Redis URI, in the form {@code redis://USERNAME:PASSWORD@host:port}. + * Any username that you set with {@link #username(String)} will override the URI. + *

+ * Note: Using this feature requires Jedis 3.6.0 or later. + * + * @param username the username for ACL authentication + * @return the builder + * @since 2.2.0 + */ + public RedisStoreBuilder username(String username) { + this.username = username; + return this; + } + /** * Specifies a password that will be sent to Redis in an AUTH command. *

- * It is also possible to include a password in the Redis URI, in the form {@code redis://:PASSWORD@host:port}. Any - * password that you set with {@link #password(String)} will override the URI. + * It is also possible to include a password in the Redis URI, in the form {@code redis://:PASSWORD@host:port} + * or {@code redis://USERNAME:PASSWORD@host:port} for ACL authentication. Any password that you set with + * {@link #password(String)} will override the URI. * * @param password the password * @return the builder diff --git a/lib/java-server-sdk-redis-store/src/main/java/com/launchdarkly/sdk/server/integrations/RedisStoreImplBase.java b/lib/java-server-sdk-redis-store/src/main/java/com/launchdarkly/sdk/server/integrations/RedisStoreImplBase.java index fa1ff5c..2241675 100644 --- a/lib/java-server-sdk-redis-store/src/main/java/com/launchdarkly/sdk/server/integrations/RedisStoreImplBase.java +++ b/lib/java-server-sdk-redis-store/src/main/java/com/launchdarkly/sdk/server/integrations/RedisStoreImplBase.java @@ -21,11 +21,15 @@ protected RedisStoreImplBase(RedisStoreBuilder builder, LDLogger logger) { // to decompose the URI. String host = builder.uri.getHost(); int port = builder.uri.getPort(); + String username = builder.username == null ? RedisURIComponents.getUsername(builder.uri) : builder.username; String password = builder.password == null ? RedisURIComponents.getPassword(builder.uri) : builder.password; int database = builder.database == null ? RedisURIComponents.getDBIndex(builder.uri) : builder.database; boolean tls = builder.tls || builder.uri.getScheme().equals("rediss"); String extra = tls ? " with TLS" : ""; + if (username != null) { + extra = extra + (extra.isEmpty() ? " with" : " and") + " username"; + } if (password != null) { extra = extra + (extra.isEmpty() ? " with" : " and") + " password"; } @@ -41,6 +45,7 @@ protected RedisStoreImplBase(RedisStoreBuilder builder, LDLogger logger) { port, (int) builder.connectTimeout.toMillis(), (int) builder.socketTimeout.toMillis(), + username, password, database, null, // clientName diff --git a/lib/java-server-sdk-redis-store/src/main/java/com/launchdarkly/sdk/server/integrations/RedisURIComponents.java b/lib/java-server-sdk-redis-store/src/main/java/com/launchdarkly/sdk/server/integrations/RedisURIComponents.java index 3c39ccc..3c44a74 100644 --- a/lib/java-server-sdk-redis-store/src/main/java/com/launchdarkly/sdk/server/integrations/RedisURIComponents.java +++ b/lib/java-server-sdk-redis-store/src/main/java/com/launchdarkly/sdk/server/integrations/RedisURIComponents.java @@ -8,6 +8,39 @@ * that class doesn't exist in the same location in both versions. */ abstract class RedisURIComponents { + /** + * Extracts the username from a Redis URI. + *

+ * Supports both formats: + *

    + *
  • {@code redis://USERNAME:PASSWORD@host:port} - returns USERNAME
  • + *
  • {@code redis://:PASSWORD@host:port} - returns null (password-only, legacy format)
  • + *
+ * + * @param uri the Redis URI + * @return the username, or null if not specified or empty + */ + static String getUsername(URI uri) { + if (uri.getUserInfo() == null) { + return null; + } + String[] parts = uri.getUserInfo().split(":", 2); + // If the username part is empty (e.g., ":password"), return null + return (parts.length > 0 && !parts[0].isEmpty()) ? parts[0] : null; + } + + /** + * Extracts the password from a Redis URI. + *

+ * Supports both formats: + *

    + *
  • {@code redis://USERNAME:PASSWORD@host:port} - returns PASSWORD
  • + *
  • {@code redis://:PASSWORD@host:port} - returns PASSWORD (legacy format)
  • + *
+ * + * @param uri the Redis URI + * @return the password, or null if not specified + */ static String getPassword(URI uri) { if (uri.getUserInfo() == null) { return null; @@ -16,6 +49,12 @@ static String getPassword(URI uri) { return parts.length < 2 ? null : parts[1]; } + /** + * Extracts the database index from a Redis URI. + * + * @param uri the Redis URI (e.g., {@code redis://host:port/2}) + * @return the database index, or 0 if not specified + */ static int getDBIndex(URI uri) { String[] parts = uri.getPath().split("/", 2); if (parts.length < 2 || parts[1].isEmpty()) {