Skip to content

Commit ad53918

Browse files
feat: add support for Dgraph connection strings (#268)
1 parent 323366e commit ad53918

File tree

4 files changed

+587
-0
lines changed

4 files changed

+587
-0
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
44

55
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.1.0/),
66

7+
## [XX.X.X] - 2025-XX-XX
8+
9+
**Added**
10+
11+
- add support for Dgraph connection strings (#268)
12+
713
## [24.1.1] - 2024-12-13
814

915
**Added**

README.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,47 @@ client and use it to mutate or query dgraph. For the async client, more details
169169

170170
### Creating a Client
171171

172+
#### Using a Connection String
173+
174+
This library supports connecting to a Dgraph cluster using connection strings. Dgraph connections
175+
strings take the form `dgraph://{username:password@}host:port?args`.
176+
177+
`username` and `password` are optional. If username is provided, a password must also be present. If
178+
supplied, these credentials are used to log into a Dgraph cluster through the ACL mechanism.
179+
180+
Valid connection string args:
181+
182+
| Arg | Value | Description |
183+
| ----------- | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
184+
| apikey | \<key\> | a Dgraph Cloud API Key |
185+
| bearertoken | \<token\> | an access token |
186+
| sslmode | disable \| require \| verify-ca | TLS option, the default is `disable`. If `verify-ca` is set, the TLS certificate configured in the Dgraph cluster must be from a valid certificate authority. |
187+
188+
Note that using `sslmode=require` disables certificate validation and significantly reduces the
189+
security of TLS. This mode should only be used in non-production (e.g., testing or development)
190+
environments.
191+
192+
Some example connection strings:
193+
194+
| Value | Explanation |
195+
| ------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------- |
196+
| dgraph://localhost:9080 | Connect to localhost, no ACL, no TLS |
197+
| dgraph://sally:supersecret@dg.example.com:443?sslmode=verify-ca | Connect to remote server, use ACL and require TLS and a valid certificate from a CA |
198+
| dgraph://foo-bar.grpc.us-west-2.aws.cloud.dgraph.io:443?sslmode=verify-ca&apikey=\<your-api-connection-key\> | Connect to a Dgraph Cloud cluster |
199+
| dgraph://foo-bar.grpc.hypermode.com?sslmode=verify-ca&bearertoken=\<some access token\> | Connect to a Dgraph cluster protected by a secure gateway |
200+
201+
Using the `DgraphClient.open` function with a connection string:
202+
203+
```java
204+
// open a connection to an ACL-enabled, non-TLS cluster and login as groot
205+
DgraphClient client = DgraphClient.open("dgraph://groot:password@localhost:8090");
206+
207+
// some time later...
208+
client.shutdown();
209+
```
210+
211+
#### Using Managed Channels
212+
172213
The following code snippet shows how to create a synchronous client using three connections.
173214

174215
```java
@@ -201,6 +242,8 @@ DgraphStub stub = DgraphClient.clientStubFromCloudEndpoint("https://your-instanc
201242
DgraphClient dgraphClient = new DgraphClient(stub);
202243
```
203244

245+
Note the `DgraphClient.open` method can be used if you have a Dgraph connection string (see above).
246+
204247
### Creating a Secure Client using TLS
205248

206249
To setup a client using TLS, you could use the following code snippet. The server needs to be setup

src/main/java/io/dgraph/DgraphClient.java

Lines changed: 302 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,20 @@
1010
import io.dgraph.DgraphProto.Version;
1111
import io.grpc.ManagedChannelBuilder;
1212
import io.grpc.Metadata;
13+
import io.grpc.netty.NettyChannelBuilder;
14+
import io.grpc.netty.GrpcSslContexts;
15+
import io.netty.handler.ssl.SslContext;
16+
import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
1317
import io.grpc.stub.MetadataUtils;
1418
import java.net.MalformedURLException;
1519
import java.net.URL;
1620
import java.util.concurrent.Executor;
21+
import java.util.HashMap;
22+
import java.util.Map;
23+
import java.io.UnsupportedEncodingException;
24+
import java.net.URLDecoder;
25+
import java.nio.charset.StandardCharsets;
26+
import javax.net.ssl.SSLException;
1727

1828
/**
1929
* Implementation of a DgraphClient using grpc.
@@ -28,9 +38,301 @@
2838
*/
2939
public class DgraphClient {
3040
private static final String gRPC_AUTHORIZATION_HEADER_NAME = "authorization";
41+
private static final String DGRAPH_SCHEME = "dgraph";
42+
private static final String SSLMODE_DISABLE = "disable";
43+
private static final String SSLMODE_REQUIRE = "require";
44+
private static final String SSLMODE_VERIFY_CA = "verify-ca";
3145

3246
private final DgraphAsyncClient asyncClient;
3347

48+
/**
49+
* Options for configuring a Dgraph client connection.
50+
*
51+
* <p>Example use:
52+
* <pre>{@code
53+
* DgraphClient client = DgraphClient.ClientOptions.forAddress("localhost", 9080)
54+
* .withACLCredentials("username", "password")
55+
* .withTLS()
56+
* .build();
57+
* }</pre>
58+
*/
59+
public static class ClientOptions {
60+
private final ManagedChannelBuilder<?> channelBuilder;
61+
private String username;
62+
private String password;
63+
private String authorizationToken;
64+
private final String host;
65+
private final int port;
66+
67+
private ClientOptions(String host, int port) {
68+
this.host = host;
69+
this.port = port;
70+
this.channelBuilder = ManagedChannelBuilder.forAddress(host, port);
71+
// Default to plaintext
72+
this.channelBuilder.usePlaintext();
73+
}
74+
75+
/**
76+
* Creates a new ClientOptions instance for the given host and port.
77+
*
78+
* @param host The hostname of the Dgraph server.
79+
* @param port The port of the Dgraph server.
80+
* @return A new ClientOptions instance.
81+
*/
82+
public static ClientOptions forAddress(String host, int port) {
83+
return new ClientOptions(host, port);
84+
}
85+
86+
/**
87+
* Sets username and password for ACL authentication.
88+
*
89+
* @param username The username for ACL authentication.
90+
* @param password The password for ACL authentication.
91+
* @return This ClientOptions instance for chaining.
92+
*/
93+
public ClientOptions withACLCredentials(String username, String password) {
94+
this.username = username;
95+
this.password = password;
96+
return this;
97+
}
98+
99+
/**
100+
* Sets a Dgraph API key for authorization.
101+
*
102+
* @param apiKey The API key to use for authorization.
103+
* @return This ClientOptions instance for chaining.
104+
*/
105+
public ClientOptions withDgraphApiKey(String apiKey) {
106+
this.authorizationToken = apiKey;
107+
return this;
108+
}
109+
110+
/**
111+
* Sets a bearer token for authorization.
112+
*
113+
* @param token The bearer token to use for authorization.
114+
* @return This ClientOptions instance for chaining.
115+
*/
116+
public ClientOptions withBearerToken(String token) {
117+
this.authorizationToken = "Bearer " + token;
118+
return this;
119+
}
120+
121+
/**
122+
* Configures the client to use plaintext communication (no encryption).
123+
*
124+
* @return This ClientOptions instance for chaining.
125+
*/
126+
public ClientOptions withPlaintext() {
127+
this.channelBuilder.usePlaintext();
128+
return this;
129+
}
130+
131+
/**
132+
* Configures the client to use TLS but skip certificate validation.
133+
* Be aware this disables certificate validation and significantly reduces the
134+
* security of TLS. This mode should only be used in non-production
135+
* (e.g., testing or development) environments.
136+
*
137+
* @return A new ClientOptions instance with TLS but without certificate validation.
138+
* @throws SSLException If there's an error configuring the SSL context.
139+
*/
140+
public ClientOptions withTLSSkipVerify() throws SSLException {
141+
SslContext sslContext = GrpcSslContexts.forClient()
142+
.trustManager(InsecureTrustManagerFactory.INSTANCE)
143+
.build();
144+
145+
// Create a new options object with the same credentials
146+
ClientOptions newOptions = new ClientOptions(host, port) {
147+
@Override
148+
public DgraphGrpc.DgraphStub createStub() {
149+
NettyChannelBuilder nettyBuilder = NettyChannelBuilder.forAddress(host, port);
150+
nettyBuilder.sslContext(sslContext);
151+
return DgraphGrpc.newStub(nettyBuilder.build());
152+
}
153+
};
154+
155+
// Copy over the auth settings
156+
newOptions.username = this.username;
157+
newOptions.password = this.password;
158+
newOptions.authorizationToken = this.authorizationToken;
159+
return newOptions;
160+
}
161+
162+
/**
163+
* Configures the client to use TLS with certificate validation.
164+
*
165+
* @return This ClientOptions instance for chaining.
166+
*/
167+
public ClientOptions withTLS() {
168+
this.channelBuilder.useTransportSecurity();
169+
return this;
170+
}
171+
172+
/**
173+
* Creates the gRPC stub based on the channel builder.
174+
* This method can be overridden by subclasses to customize stub creation.
175+
*/
176+
protected DgraphGrpc.DgraphStub createStub() {
177+
return DgraphGrpc.newStub(channelBuilder.build());
178+
}
179+
180+
/**
181+
* Creates a new DgraphClient with the configured options.
182+
*
183+
* @return A new DgraphClient instance.
184+
*/
185+
public DgraphClient build() {
186+
DgraphGrpc.DgraphStub stub = createStub();
187+
188+
if (authorizationToken != null) {
189+
Metadata metadata = new Metadata();
190+
metadata.put(
191+
Metadata.Key.of(gRPC_AUTHORIZATION_HEADER_NAME, Metadata.ASCII_STRING_MARSHALLER),
192+
authorizationToken);
193+
stub = stub.withInterceptors(MetadataUtils.newAttachHeadersInterceptor(metadata));
194+
}
195+
196+
DgraphClient client = new DgraphClient(stub);
197+
198+
if (username != null && password != null) {
199+
client.login(username, password);
200+
}
201+
202+
return client;
203+
}
204+
}
205+
206+
/**
207+
* Parses query parameters from a URL.
208+
*
209+
* @param url The URL containing query parameters
210+
* @return A map of parameter names to values
211+
* @throws IllegalStateException If UTF-8 encoding is not supported by the JVM (should never happen)
212+
*/
213+
private static Map<String, String> parseQueryParameters(URL url) {
214+
Map<String, String> params = new HashMap<>();
215+
if (url.getQuery() == null) {
216+
return params;
217+
}
218+
219+
String[] pairs = url.getQuery().split("&");
220+
for (String pair : pairs) {
221+
int idx = pair.indexOf("=");
222+
if (idx > 0) {
223+
try {
224+
String key = URLDecoder.decode(pair.substring(0, idx), StandardCharsets.UTF_8.toString());
225+
String value = URLDecoder.decode(pair.substring(idx + 1), StandardCharsets.UTF_8.toString());
226+
params.put(key, value);
227+
} catch (UnsupportedEncodingException e) {
228+
throw new IllegalStateException("UTF-8 encoding not supported by the JVM", e);
229+
}
230+
}
231+
}
232+
return params;
233+
}
234+
235+
/**
236+
* Creates a new DgraphClient instance from a connection string.
237+
*
238+
* <p>This method attempts to authenticate via Dgraph's ACL mechanism if
239+
* username and password are provided.
240+
* <p>The connection string has the format: "dgraph://[username:password@]host:port[?params]"
241+
* <p>Supported query parameters:
242+
* <ul>
243+
* <li>sslmode - SSL connection mode. Supported values:
244+
* <ul>
245+
* <li>"disable" - No encryption, uses plaintext</li>
246+
* <li>"require" - Uses TLS encryption without certificate verification</li>
247+
* <li>"verify-ca" - Uses TLS encryption with certificate verification</li>
248+
* </ul>
249+
* </li>
250+
* <li>apikey - API key for authorization from Dgraph Cloud</li>
251+
* <li>bearertoken - Bearer token for authorization</li>
252+
* </ul>
253+
*
254+
* @param connectionString The connection string to connect to Dgraph
255+
* @return A new DgraphClient instance
256+
* @throws IllegalArgumentException If the connection string is invalid
257+
* @throws MalformedURLException If the connection string cannot be parsed as a URL
258+
* @throws SSLException If there's an error configuring the SSL context for sslmode=require
259+
* @throws IllegalStateException If UTF-8 encoding is not supported by the JVM (should never happen)
260+
*/
261+
public static DgraphClient open(String connectionString)
262+
throws IllegalArgumentException, MalformedURLException, SSLException, IllegalStateException {
263+
if (connectionString == null || connectionString.isEmpty()) {
264+
throw new IllegalArgumentException("Connection string cannot be null or empty");
265+
}
266+
267+
// Connection string format: dgraph://[username:password@]host:port[?params]
268+
if (!connectionString.startsWith(DGRAPH_SCHEME + "://")) {
269+
throw new IllegalArgumentException("Invalid connection string: scheme must be '" + DGRAPH_SCHEME + "'");
270+
}
271+
272+
URL url;
273+
try {
274+
// Replace dgraph:// with http:// for proper URL parsing
275+
url = new URL(connectionString.replace(DGRAPH_SCHEME + "://", "http://"));
276+
} catch (MalformedURLException e) {
277+
throw new IllegalArgumentException("Failed to parse connection string: " + e.getMessage(), e);
278+
}
279+
280+
String host = url.getHost();
281+
int port = url.getPort();
282+
283+
if (host == null || host.isEmpty()) {
284+
throw new IllegalArgumentException("Invalid connection string: hostname required");
285+
}
286+
if (port == -1) {
287+
throw new IllegalArgumentException("Invalid connection string: port required");
288+
}
289+
290+
ClientOptions options = ClientOptions.forAddress(host, port);
291+
292+
if (url.getUserInfo() != null) {
293+
String[] userInfo = url.getUserInfo().split(":", 2);
294+
String username = userInfo[0];
295+
String password = null;
296+
if (userInfo.length > 1) {
297+
password = userInfo[1];
298+
}
299+
if (username != null && (password == null || password.isEmpty())) {
300+
throw new IllegalArgumentException(
301+
"Invalid connection string: password required when username is provided");
302+
}
303+
options.withACLCredentials(username, password);
304+
}
305+
306+
Map<String, String> params = parseQueryParameters(url);
307+
308+
if (params.containsKey("sslmode")) {
309+
String sslmode = params.get("sslmode");
310+
if (SSLMODE_DISABLE.equals(sslmode)) {
311+
options.withPlaintext();
312+
} else if (SSLMODE_REQUIRE.equals(sslmode)) {
313+
// This assignment is necessary to reassign the overridden createStub method
314+
options = options.withTLSSkipVerify();
315+
} else if (SSLMODE_VERIFY_CA.equals(sslmode)) {
316+
options.withTLS();
317+
} else {
318+
throw new IllegalArgumentException("Invalid sslmode: " + sslmode);
319+
}
320+
}
321+
322+
if (params.containsKey("apikey") && params.containsKey("bearertoken")) {
323+
throw new IllegalArgumentException(
324+
"apikey and bearertoken cannot both be provided");
325+
}
326+
327+
if (params.containsKey("apikey")) {
328+
options.withDgraphApiKey(params.get("apikey"));
329+
} else if (params.containsKey("bearertoken")) {
330+
options.withBearerToken(params.get("bearertoken"));
331+
}
332+
333+
return options.build();
334+
}
335+
34336
/**
35337
* Creates a gRPC stub that can be used to construct clients to connect with Slash GraphQL.
36338
*

0 commit comments

Comments
 (0)