Skip to content

Commit 75625f3

Browse files
authored
feat(java-server-sdk-redis-store): Add username/password authentication support for Redis 6.0+ (#96)
- Add username field and builder method to RedisStoreBuilder - Update JedisPool constructor to pass username parameter - Add getUsername() method to RedisURIComponents for URI parsing - Update Jedis dependency to 7.1.0 (minimum 3.6.0 for ACL support) - Update README with authentication documentation This enables Redis 6.0+ ACL authentication with username/password pairs, in addition to the existing password-only authentication. **Requirements** - [X] I have added test coverage for new or changed functionality - [X] I have followed the repository's [pull request submission guidelines](../blob/main/CONTRIBUTING.md#submitting-pull-requests) - [X] I have validated my changes against all supported platform versions **Related issues** Provide links to any issues in this repository or elsewhere relating to this pull request. **Describe the solution you've provided** Provide a clear and concise description of what you expect to happen. **Describe alternatives you've considered** Provide a clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context about the pull request here. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Adds ACL username/password authentication support and updates Jedis. > > - Introduces `username` field and `username(String)` in `RedisStoreBuilder`; updates `RedisStoreImplBase` to pass `username` to `JedisPool` > - Adds `RedisURIComponents.getUsername` and extends URI parsing; updates tests for username/password and explicit port usage > - Bumps default Jedis to `7.1.0`; CI matrix tests `3.6.0` and `7.1.0`; adjusts sed in workflow > - Updates README with password-only and username/password auth examples and notes on Jedis `3.6.0+` requirement > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 2a1200c. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 8d2d922 commit 75625f3

File tree

9 files changed

+137
-13
lines changed

9 files changed

+137
-13
lines changed

.github/workflows/java-server-sdk-redis-store.yml

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ jobs:
1414
build-test-java-server-sdk-redis-store:
1515
strategy:
1616
matrix:
17-
jedis-version: [2.9.0, 3.0.0]
17+
# Username/password (Redis ACL) support requires Jedis 3.6.0+
18+
jedis-version: [3.6.0, 7.1.0]
1819
runs-on: ubuntu-latest
1920
steps:
2021
- uses: actions/checkout@v3
@@ -23,7 +24,7 @@ jobs:
2324
shell: bash
2425
run: |
2526
cd lib/java-server-sdk-redis-store
26-
sed -i.bak 's#"jedis":.*"[0-9.]*"#"jedis":"${{ matrix.jedis-version }}"#' build.gradle
27+
sed -i.bak -E 's/"jedis":[[:space:]]*"[0-9.]+"/"jedis": "${{ matrix.jedis-version }}"/' build.gradle
2728
2829
- name: Shared CI Steps
2930
uses: ./.github/actions/ci
@@ -34,7 +35,8 @@ jobs:
3435
build-test-java-server-sdk-windows:
3536
strategy:
3637
matrix:
37-
jedis-version: [2.9.0, 3.0.0]
38+
# Username/password (Redis ACL) support requires Jedis 3.6.0+
39+
jedis-version: [3.6.0, 7.1.0]
3840
runs-on: windows-latest
3941
steps:
4042
- uses: actions/checkout@v3
@@ -54,7 +56,7 @@ jobs:
5456
shell: bash
5557
run: |
5658
cd lib/java-server-sdk-redis-store
57-
sed -i.bak 's#"jedis":.*"[0-9.]*"#"jedis":"${{ matrix.jedis-version }}"#' build.gradle
59+
sed -i.bak -E 's/"jedis":[[:space:]]*"[0-9.]+"/"jedis": "${{ matrix.jedis-version }}"/' build.gradle
5860
5961
- name: Setup Java
6062
uses: actions/setup-java@v4

lib/java-server-sdk-redis-store/README.md

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,10 @@ This assumes that you have already installed the LaunchDarkly Java SDK.
2323
<dependency>
2424
<groupId>redis.clients</groupId>
2525
<artifactId>jedis</artifactId>
26-
<version>2.9.0</version>
26+
<version>7.1.0</version>
2727
</dependency>
2828

29-
This library is compatible with Jedis 2.x versions greater than or equal to 2.9.0, and also with Jedis 3.x.
29+
This library uses Jedis 7.1.0 by default and requires Jedis 3.6.0 or later for username/password (ACL) authentication support.
3030

3131
3. Import the LaunchDarkly package and the package for this library:
3232

@@ -45,6 +45,44 @@ This assumes that you have already installed the LaunchDarkly Java SDK.
4545

4646
By default, the store will try to connect to a local Redis instance on port 6379.
4747

48+
## Authentication
49+
50+
### Password-only authentication (legacy)
51+
52+
For Redis servers using simple password authentication:
53+
54+
LDConfig config = new LDConfig.Builder()
55+
.dataStore(
56+
Components.persistentDataStore(
57+
Redis.dataStore().password("my-redis-password")
58+
)
59+
)
60+
.build();
61+
62+
Or include it in the URI:
63+
64+
Redis.dataStore().url("redis://:my-redis-password@my-redis-host:6379")
65+
66+
### Username/password authentication (Redis 6.0+ ACL)
67+
68+
For Redis 6.0+ servers using ACL with username and password:
69+
70+
LDConfig config = new LDConfig.Builder()
71+
.dataStore(
72+
Components.persistentDataStore(
73+
Redis.dataStore()
74+
.username("my-username")
75+
.password("my-password")
76+
)
77+
)
78+
.build();
79+
80+
Or include both in the URI:
81+
82+
Redis.dataStore().url("redis://my-username:my-password@my-redis-host:6379")
83+
84+
**Note:** Username/password authentication requires Jedis 3.6.0 or later.
85+
4886
## Caching behavior
4987

5088
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:

lib/java-server-sdk-redis-store/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ ext {
4242

4343
ext.versions = [
4444
"sdk": "6.3.0", // the *lowest* version we're compatible with
45-
"jedis": "2.9.0"
45+
"jedis": "7.1.0" // 3.6.0+ required for username/password (ACL) authentication
4646
]
4747

4848
ext.libraries = [:]

lib/java-server-sdk-redis-store/src/main/java/com/launchdarkly/sdk/server/integrations/RedisStoreBuilder.java

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ public abstract class RedisStoreBuilder<T> implements ComponentConfigurer<T>, Di
7676
Duration connectTimeout = Duration.ofMillis(Protocol.DEFAULT_TIMEOUT);
7777
Duration socketTimeout = Duration.ofMillis(Protocol.DEFAULT_TIMEOUT);
7878
Integer database = null;
79+
String username = null;
7980
String password = null;
8081
boolean tls = false;
8182
JedisPoolConfig poolConfig = null;
@@ -98,11 +99,30 @@ public RedisStoreBuilder<T> database(Integer database) {
9899
return this;
99100
}
100101

102+
/**
103+
* Specifies a username for Redis ACL authentication.
104+
* <p>
105+
* Redis 6.0+ supports Access Control Lists (ACL) with username/password authentication.
106+
* It is also possible to include a username in the Redis URI, in the form {@code redis://USERNAME:PASSWORD@host:port}.
107+
* Any username that you set with {@link #username(String)} will override the URI.
108+
* <p>
109+
* Note: Using this feature requires Jedis 3.6.0 or later.
110+
*
111+
* @param username the username for ACL authentication
112+
* @return the builder
113+
* @since 2.2.0
114+
*/
115+
public RedisStoreBuilder<T> username(String username) {
116+
this.username = username;
117+
return this;
118+
}
119+
101120
/**
102121
* Specifies a password that will be sent to Redis in an AUTH command.
103122
* <p>
104-
* It is also possible to include a password in the Redis URI, in the form {@code redis://:PASSWORD@host:port}. Any
105-
* password that you set with {@link #password(String)} will override the URI.
123+
* It is also possible to include a password in the Redis URI, in the form {@code redis://:PASSWORD@host:port}
124+
* or {@code redis://USERNAME:PASSWORD@host:port} for ACL authentication. Any password that you set with
125+
* {@link #password(String)} will override the URI.
106126
*
107127
* @param password the password
108128
* @return the builder

lib/java-server-sdk-redis-store/src/main/java/com/launchdarkly/sdk/server/integrations/RedisStoreImplBase.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,15 @@ protected RedisStoreImplBase(RedisStoreBuilder<?> builder, LDLogger logger) {
2121
// to decompose the URI.
2222
String host = builder.uri.getHost();
2323
int port = builder.uri.getPort();
24+
String username = builder.username == null ? RedisURIComponents.getUsername(builder.uri) : builder.username;
2425
String password = builder.password == null ? RedisURIComponents.getPassword(builder.uri) : builder.password;
2526
int database = builder.database == null ? RedisURIComponents.getDBIndex(builder.uri) : builder.database;
2627
boolean tls = builder.tls || builder.uri.getScheme().equals("rediss");
2728

2829
String extra = tls ? " with TLS" : "";
30+
if (username != null) {
31+
extra = extra + (extra.isEmpty() ? " with" : " and") + " username";
32+
}
2933
if (password != null) {
3034
extra = extra + (extra.isEmpty() ? " with" : " and") + " password";
3135
}
@@ -41,6 +45,7 @@ protected RedisStoreImplBase(RedisStoreBuilder<?> builder, LDLogger logger) {
4145
port,
4246
(int) builder.connectTimeout.toMillis(),
4347
(int) builder.socketTimeout.toMillis(),
48+
username,
4449
password,
4550
database,
4651
null, // clientName

lib/java-server-sdk-redis-store/src/main/java/com/launchdarkly/sdk/server/integrations/RedisURIComponents.java

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,39 @@
88
* that class doesn't exist in the same location in both versions.
99
*/
1010
abstract class RedisURIComponents {
11+
/**
12+
* Extracts the username from a Redis URI.
13+
* <p>
14+
* Supports both formats:
15+
* <ul>
16+
* <li>{@code redis://USERNAME:PASSWORD@host:port} - returns USERNAME</li>
17+
* <li>{@code redis://:PASSWORD@host:port} - returns null (password-only, legacy format)</li>
18+
* </ul>
19+
*
20+
* @param uri the Redis URI
21+
* @return the username, or null if not specified or empty
22+
*/
23+
static String getUsername(URI uri) {
24+
if (uri.getUserInfo() == null) {
25+
return null;
26+
}
27+
String[] parts = uri.getUserInfo().split(":", 2);
28+
// If the username part is empty (e.g., ":password"), return null
29+
return (parts.length > 0 && !parts[0].isEmpty()) ? parts[0] : null;
30+
}
31+
32+
/**
33+
* Extracts the password from a Redis URI.
34+
* <p>
35+
* Supports both formats:
36+
* <ul>
37+
* <li>{@code redis://USERNAME:PASSWORD@host:port} - returns PASSWORD</li>
38+
* <li>{@code redis://:PASSWORD@host:port} - returns PASSWORD (legacy format)</li>
39+
* </ul>
40+
*
41+
* @param uri the Redis URI
42+
* @return the password, or null if not specified
43+
*/
1144
static String getPassword(URI uri) {
1245
if (uri.getUserInfo() == null) {
1346
return null;
@@ -16,6 +49,12 @@ static String getPassword(URI uri) {
1649
return parts.length < 2 ? null : parts[1];
1750
}
1851

52+
/**
53+
* Extracts the database index from a Redis URI.
54+
*
55+
* @param uri the Redis URI (e.g., {@code redis://host:port/2})
56+
* @return the database index, or 0 if not specified
57+
*/
1958
static int getDBIndex(URI uri) {
2059
String[] parts = uri.getPath().split("/", 2);
2160
if (parts.length < 2 || parts[1].isEmpty()) {

lib/java-server-sdk-redis-store/src/test/java/com/launchdarkly/sdk/server/integrations/RedisBigSegmentStoreImplTest.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ protected ComponentConfigurer<BigSegmentStore> makeStore(String prefix) {
1717
@Override
1818
protected void clearData(String prefix) {
1919
prefix = prefix == null || prefix.isEmpty() ? RedisStoreBuilder.DEFAULT_PREFIX : prefix;
20-
try (Jedis client = new Jedis("localhost")) {
20+
try (Jedis client = new Jedis("localhost", 6379)) {
2121
for (String key : client.keys(prefix + ":*")) {
2222
client.del(key);
2323
}
@@ -26,7 +26,7 @@ protected void clearData(String prefix) {
2626

2727
@Override
2828
protected void setMetadata(String prefix, BigSegmentStoreTypes.StoreMetadata storeMetadata) {
29-
try (Jedis client = new Jedis("localhost")) {
29+
try (Jedis client = new Jedis("localhost", 6379)) {
3030
client.set(prefix + ":big_segments_synchronized_on",
3131
storeMetadata != null ? Long.toString(storeMetadata.getLastUpToDate()) : "");
3232
}
@@ -37,7 +37,7 @@ protected void setSegments(String prefix,
3737
String userHashKey,
3838
Iterable<String> includedSegmentRefs,
3939
Iterable<String> excludedSegmentRefs) {
40-
try (Jedis client = new Jedis("localhost")) {
40+
try (Jedis client = new Jedis("localhost", 6379)) {
4141
String includeKey = prefix + ":big_segment_include:" + userHashKey;
4242
String excludeKey = prefix + ":big_segment_exclude:" + userHashKey;
4343
for (String includedSegmentRef : includedSegmentRefs) {

lib/java-server-sdk-redis-store/src/test/java/com/launchdarkly/sdk/server/integrations/RedisDataStoreImplTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ protected ComponentConfigurer<PersistentDataStore> buildStore(String prefix) {
2020

2121
@Override
2222
protected void clearAllData() {
23-
try (Jedis client = new Jedis("localhost")) {
23+
try (Jedis client = new Jedis("localhost", 6379)) {
2424
client.flushDB();
2525
}
2626
}

lib/java-server-sdk-redis-store/src/test/java/com/launchdarkly/sdk/server/integrations/RedisURIComponentsTest.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,26 @@
88
import static org.junit.Assert.assertNull;
99

1010
public class RedisURIComponentsTest {
11+
@Test
12+
public void getUsernameForURIWithoutUserInfo() {
13+
assertNull(RedisURIComponents.getUsername(URI.create("redis://hostname:6379")));
14+
}
15+
16+
@Test
17+
public void getUsernameForURIWithUsernameAndNoPassword() {
18+
assertEquals("username", RedisURIComponents.getUsername(URI.create("redis://username@hostname:6379")));
19+
}
20+
21+
@Test
22+
public void getUsernameForURIWithUsernameAndPassword() {
23+
assertEquals("username", RedisURIComponents.getUsername(URI.create("redis://username:secret@hostname:6379")));
24+
}
25+
26+
@Test
27+
public void getUsernameForURIWithPasswordAndNoUsername() {
28+
assertNull(RedisURIComponents.getUsername(URI.create("redis://:secret@hostname:6379")));
29+
}
30+
1131
@Test
1232
public void getPasswordForURIWithoutUserInfo() {
1333
assertNull(RedisURIComponents.getPassword(URI.create("redis://hostname:6379")));

0 commit comments

Comments
 (0)