Skip to content

Commit 839e6ed

Browse files
jhuynh1sobychacko
authored andcommitted
Add username and password authentication to GemFire Vector Store
Refactor tests to run queries on non authenticated cluster Authenticated tests will confirm cluster can authenticate on basic operation Added authentication properties to auto configuration and adoc Signed-off-by: Jason Huynh <jason.huynh@broadcom.com>
1 parent 147f85a commit 839e6ed

File tree

14 files changed

+633
-6
lines changed

14 files changed

+633
-6
lines changed

auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-gemfire/src/main/java/org/springframework/ai/vectorstore/gemfire/autoconfigure/GemFireConnectionDetails.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,16 @@ public interface GemFireConnectionDetails extends ConnectionDetails {
2929

3030
int getPort();
3131

32+
default String getUsername() {
33+
return null;
34+
}
35+
36+
default String getPassword() {
37+
return null;
38+
}
39+
40+
default String getToken() {
41+
return null;
42+
}
43+
3244
}

auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-gemfire/src/main/java/org/springframework/ai/vectorstore/gemfire/autoconfigure/GemFireVectorStoreAutoConfiguration.java

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2023-2024 the original author or authors.
2+
* Copyright 2023-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -38,6 +38,7 @@
3838
* @author Geet Rawat
3939
* @author Christian Tzolov
4040
* @author Soby Chacko
41+
* @author Jason Huynh
4142
*/
4243
@AutoConfiguration
4344
@ConditionalOnClass({ GemFireVectorStore.class, EmbeddingModel.class })
@@ -80,6 +81,9 @@ public GemFireVectorStore gemfireVectorStore(EmbeddingModel embeddingModel, GemF
8081
.observationRegistry(observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP))
8182
.customObservationConvention(customObservationConvention.getIfAvailable(() -> null))
8283
.batchingStrategy(batchingStrategy)
84+
.username(gemFireConnectionDetails.getUsername())
85+
.password(gemFireConnectionDetails.getPassword())
86+
.token(gemFireConnectionDetails.getToken())
8387
.build();
8488
}
8589

@@ -101,6 +105,21 @@ public int getPort() {
101105
return this.properties.getPort();
102106
}
103107

108+
@Override
109+
public String getUsername() {
110+
return this.properties.getUsername();
111+
}
112+
113+
@Override
114+
public String getPassword() {
115+
return this.properties.getPassword();
116+
}
117+
118+
@Override
119+
public String getToken() {
120+
return this.properties.getToken();
121+
}
122+
104123
}
105124

106125
}

auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-gemfire/src/main/java/org/springframework/ai/vectorstore/gemfire/autoconfigure/GemFireVectorStoreProperties.java

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,27 @@ public class GemFireVectorStoreProperties extends CommonVectorStoreProperties {
9595
*/
9696
private boolean sslEnabled = GemFireVectorStore.DEFAULT_SSL_ENABLED;
9797

98+
/**
99+
* Configures the username for the GemFire VectorStore connection
100+
*
101+
* To specify username, use "spring.ai.vectorstore.gemfire.username";
102+
*/
103+
private String username;
104+
105+
/**
106+
* Configures the password for the GemFire VectorStore connection
107+
*
108+
* To specify password, use "spring.ai.vectorstore.gemfire.password";
109+
*/
110+
private String password;
111+
112+
/**
113+
* Configures the token for the GemFire VectorStore connection
114+
*
115+
* To specify token, use "spring.ai.vectorstore.gemfire.token";
116+
*/
117+
private String token;
118+
98119
public int getBeamWidth() {
99120
return this.beamWidth;
100121
}
@@ -167,4 +188,28 @@ public void setSslEnabled(boolean sslEnabled) {
167188
this.sslEnabled = sslEnabled;
168189
}
169190

191+
public String getToken() {
192+
return this.token;
193+
}
194+
195+
public void setToken(String token) {
196+
this.token = token;
197+
}
198+
199+
public String getPassword() {
200+
return this.password;
201+
}
202+
203+
public void setPassword(String password) {
204+
this.password = password;
205+
}
206+
207+
public String getUsername() {
208+
return this.username;
209+
}
210+
211+
public void setUsername(String username) {
212+
this.username = username;
213+
}
214+
170215
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
/*
2+
* Copyright 2023-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.ai.vectorstore.gemfire.autoconfigure;
18+
19+
import java.util.HashMap;
20+
import java.util.Map;
21+
22+
import com.fasterxml.jackson.databind.JsonNode;
23+
import com.fasterxml.jackson.databind.ObjectMapper;
24+
import com.github.dockerjava.api.model.ExposedPort;
25+
import com.github.dockerjava.api.model.PortBinding;
26+
import com.github.dockerjava.api.model.Ports;
27+
import com.vmware.gemfire.testcontainers.GemFireCluster;
28+
import io.micrometer.observation.tck.TestObservationRegistry;
29+
import org.junit.jupiter.api.AfterAll;
30+
import org.junit.jupiter.api.Assertions;
31+
import org.junit.jupiter.api.BeforeAll;
32+
import org.junit.jupiter.api.Test;
33+
34+
import org.springframework.ai.embedding.EmbeddingModel;
35+
import org.springframework.ai.transformers.TransformersEmbeddingModel;
36+
import org.springframework.ai.vectorstore.gemfire.GemFireVectorStore;
37+
import org.springframework.boot.autoconfigure.AutoConfigurations;
38+
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
39+
import org.springframework.context.annotation.Bean;
40+
import org.springframework.context.annotation.Configuration;
41+
42+
import static org.assertj.core.api.Assertions.assertThat;
43+
44+
/**
45+
* @author Geet Rawat
46+
* @author Christian Tzolov
47+
* @author Thomas Vitale
48+
*/
49+
class GemFireVectorStoreAutoConfigurationAuthenticationIT {
50+
51+
private static final String INDEX_NAME = "spring-ai-index";
52+
53+
private static final int BEAM_WIDTH = 50;
54+
55+
private static final int MAX_CONNECTIONS = 8;
56+
57+
private static final String SIMILARITY_FUNCTION = "DOT_PRODUCT";
58+
59+
private static final String[] FIELDS = { "someField1", "someField2" };
60+
61+
private static final int BUCKET_COUNT = 2;
62+
63+
private static final int HTTP_SERVICE_PORT = 9090;
64+
65+
private static final int LOCATOR_COUNT = 1;
66+
67+
private static final int SERVER_COUNT = 1;
68+
69+
private static GemFireCluster gemFireCluster;
70+
71+
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
72+
.withConfiguration(AutoConfigurations.of(GemFireVectorStoreAutoConfiguration.class))
73+
.withUserConfiguration(Config.class)
74+
.withPropertyValues("spring.ai.vectorstore.gemfire.index-name=" + INDEX_NAME)
75+
.withPropertyValues("spring.ai.vectorstore.gemfire.beam-width=" + BEAM_WIDTH)
76+
.withPropertyValues("spring.ai.vectorstore.gemfire.max-connections=" + MAX_CONNECTIONS)
77+
.withPropertyValues("spring.ai.vectorstore.gemfire.vector-similarity-function=" + SIMILARITY_FUNCTION)
78+
.withPropertyValues("spring.ai.vectorstore.gemfire.buckets=" + BUCKET_COUNT)
79+
.withPropertyValues("spring.ai.vectorstore.gemfire.fields=someField1,someField2")
80+
.withPropertyValues("spring.ai.vectorstore.gemfire.host=localhost")
81+
.withPropertyValues("spring.ai.vectorstore.gemfire.port=" + HTTP_SERVICE_PORT)
82+
.withPropertyValues("spring.ai.vectorstore.gemfire.initialize-schema=true")
83+
.withPropertyValues("spring.ai.vectorstore.gemfire.username=clusterManage,dataRead")
84+
.withPropertyValues("spring.ai.vectorstore.gemfire.password=clusterManage,dataRead")
85+
.withPropertyValues("spring.ai.vectorstore.gemfire.token=0123456789012345678901234567890");
86+
87+
@AfterAll
88+
public static void stopGemFireCluster() {
89+
gemFireCluster.close();
90+
}
91+
92+
@BeforeAll
93+
public static void startGemFireCluster() {
94+
Ports.Binding hostPort = Ports.Binding.bindPort(HTTP_SERVICE_PORT);
95+
ExposedPort exposedPort = new ExposedPort(HTTP_SERVICE_PORT);
96+
PortBinding mappedPort = new PortBinding(hostPort, exposedPort);
97+
gemFireCluster = new GemFireCluster("gemfire/gemfire-all:10.1-jdk17", LOCATOR_COUNT, SERVER_COUNT);
98+
gemFireCluster.withConfiguration(GemFireCluster.SERVER_GLOB,
99+
container -> container.withExposedPorts(HTTP_SERVICE_PORT)
100+
.withCreateContainerCmdModifier(cmd -> cmd.getHostConfig().withPortBindings(mappedPort)));
101+
gemFireCluster.withGemFireProperty(GemFireCluster.SERVER_GLOB, "http-service-port",
102+
Integer.toString(HTTP_SERVICE_PORT));
103+
gemFireCluster.withGemFireProperty(GemFireCluster.ALL_GLOB, "security-manager",
104+
"org.apache.geode.examples.SimpleSecurityManager");
105+
106+
gemFireCluster.withGemFireProperty(GemFireCluster.ALL_GLOB, "security-username", "clusterManage");
107+
gemFireCluster.withGemFireProperty(GemFireCluster.ALL_GLOB, "security-password", "clusterManage");
108+
gemFireCluster.acceptLicense().start();
109+
110+
System.setProperty("spring.data.gemfire.pool.locators",
111+
String.format("localhost[%d]", gemFireCluster.getLocatorPort()));
112+
}
113+
114+
@Test
115+
void ensureGemFireVectorStoreCustomConfiguration() {
116+
this.contextRunner.run(context -> {
117+
GemFireVectorStore store = context.getBean(GemFireVectorStore.class);
118+
119+
Assertions.assertNotNull(store);
120+
assertThat(store.getIndexName()).isEqualTo(INDEX_NAME);
121+
assertThat(store.getBeamWidth()).isEqualTo(BEAM_WIDTH);
122+
assertThat(store.getMaxConnections()).isEqualTo(MAX_CONNECTIONS);
123+
assertThat(store.getVectorSimilarityFunction()).isEqualTo(SIMILARITY_FUNCTION);
124+
assertThat(store.getFields()).isEqualTo(FIELDS);
125+
126+
String indexJson = store.getIndex();
127+
Map<String, Object> index = parseIndex(indexJson);
128+
assertThat(index.get("name")).isEqualTo(INDEX_NAME);
129+
assertThat(index.get("beam-width")).isEqualTo(BEAM_WIDTH);
130+
assertThat(index.get("max-connections")).isEqualTo(MAX_CONNECTIONS);
131+
assertThat(index.get("vector-similarity-function")).isEqualTo(SIMILARITY_FUNCTION);
132+
assertThat(index.get("buckets")).isEqualTo(BUCKET_COUNT);
133+
});
134+
}
135+
136+
private Map<String, Object> parseIndex(String json) {
137+
try {
138+
JsonNode rootNode = new ObjectMapper().readTree(json);
139+
Map<String, Object> indexDetails = new HashMap<>();
140+
if (rootNode.isObject()) {
141+
if (rootNode.has("name")) {
142+
indexDetails.put("name", rootNode.get("name").asText());
143+
}
144+
if (rootNode.has("beam-width")) {
145+
indexDetails.put("beam-width", rootNode.get("beam-width").asInt());
146+
}
147+
if (rootNode.has("max-connections")) {
148+
indexDetails.put("max-connections", rootNode.get("max-connections").asInt());
149+
}
150+
if (rootNode.has("vector-similarity-function")) {
151+
indexDetails.put("vector-similarity-function", rootNode.get("vector-similarity-function").asText());
152+
}
153+
if (rootNode.has("buckets")) {
154+
indexDetails.put("buckets", rootNode.get("buckets").asInt());
155+
}
156+
if (rootNode.has("number-of-embeddings")) {
157+
indexDetails.put("number-of-embeddings", rootNode.get("number-of-embeddings").asInt());
158+
}
159+
}
160+
return indexDetails;
161+
}
162+
catch (Exception e) {
163+
return new HashMap<>();
164+
}
165+
}
166+
167+
@Configuration(proxyBeanMethods = false)
168+
static class Config {
169+
170+
@Bean
171+
public TestObservationRegistry observationRegistry() {
172+
return TestObservationRegistry.create();
173+
}
174+
175+
@Bean
176+
public EmbeddingModel embeddingModel() {
177+
return new TransformersEmbeddingModel();
178+
}
179+
180+
}
181+
182+
}

auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-gemfire/src/test/java/org/springframework/ai/vectorstore/gemfire/autoconfigure/GemFireVectorStorePropertiesTests.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ void defaultValues() {
3838
assertThat(props.getMaxConnections()).isEqualTo(GemFireVectorStore.DEFAULT_MAX_CONNECTIONS);
3939
assertThat(props.getFields()).isEqualTo(GemFireVectorStore.DEFAULT_FIELDS);
4040
assertThat(props.getBuckets()).isEqualTo(GemFireVectorStore.DEFAULT_BUCKETS);
41+
assertThat(props.getUsername()).isNull();
42+
assertThat(props.getPassword()).isNull();
43+
assertThat(props.getToken()).isNull();
4144
}
4245

4346
@Test
@@ -50,6 +53,9 @@ void customValues() {
5053
props.setMaxConnections(10);
5154
props.setFields(new String[] { "test" });
5255
props.setBuckets(10);
56+
props.setUsername("username");
57+
props.setPassword("password");
58+
props.setToken("token");
5359

5460
assertThat(props.getIndexName()).isEqualTo("spring-ai-index");
5561
assertThat(props.getHost()).isEqualTo("localhost");
@@ -58,6 +64,9 @@ void customValues() {
5864
assertThat(props.getMaxConnections()).isEqualTo(10);
5965
assertThat(props.getFields()).isEqualTo(new String[] { "test" });
6066
assertThat(props.getBuckets()).isEqualTo(10);
67+
assertThat(props.getUsername()).isEqualTo("username");
68+
assertThat(props.getPassword()).isEqualTo("password");
69+
assertThat(props.getToken()).isEqualTo("token");
6170

6271
}
6372

spring-ai-docs/src/main/antora/modules/ROOT/pages/api/vectordbs/gemfire.adoc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ You can use the following properties in your Spring Boot configuration to furthe
5858
|`spring.ai.vectorstore.gemfire.vector-similarity-function`|COSINE
5959
|`spring.ai.vectorstore.gemfire.fields`|[]
6060
|`spring.ai.vectorstore.gemfire.buckets`|0
61+
|`spring.ai.vectorstore.gemfire.username`|null
62+
|`spring.ai.vectorstore.gemfire.password`|null
63+
|`spring.ai.vectorstore.gemfire.token`|null
6164
|===
6265

6366

@@ -93,6 +96,8 @@ public GemFireVectorStore vectorStore(EmbeddingModel embeddingModel) {
9396
return GemFireVectorStore.builder(embeddingModel)
9497
.host("localhost")
9598
.port(7071)
99+
.username("my-user-name")
100+
.password("my-password")
96101
.indexName("my-vector-index")
97102
.fields(new String[] {"country", "year", "activationDate"}) // Optional: fields for metadata filtering
98103
.initializeSchema(true)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Copyright 2023-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.ai.vectorstore.gemfire;
18+
19+
import reactor.core.publisher.Mono;
20+
21+
import org.springframework.web.reactive.function.client.ClientRequest;
22+
import org.springframework.web.reactive.function.client.ClientResponse;
23+
import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
24+
import org.springframework.web.reactive.function.client.ExchangeFunction;
25+
26+
public class BearerTokenAuthenticationFilterFunction implements ExchangeFilterFunction {
27+
28+
private final String token;
29+
30+
public BearerTokenAuthenticationFilterFunction(String token) {
31+
this.token = token;
32+
}
33+
34+
@Override
35+
public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next) {
36+
ClientRequest filteredRequest = ClientRequest.from(request)
37+
.headers(headers -> headers.setBearerAuth(this.token))
38+
.build();
39+
return next.exchange(filteredRequest);
40+
}
41+
42+
}

0 commit comments

Comments
 (0)