Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 33 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,25 +130,50 @@ public final class UmaDbExample {
}
```

### Using TLS and an API key
### Using TLS and API Key

For secured deployments (TLS + API key), the builder makes this explicit and safe:
To use a secured communication over TLS, simply enable TLS when building the UmaDbClient:

```java
UmaDbClient client = UmaDbClient.builder()
.withHost("localhost")
.withPort(50051)
.withTlsAndApiKey(
"server.pem", // CA certificate
"umadb:example-api-key-123456789" // API key
)
.withTlsEnabled()
.build();

client.connect();
```

> ⚠️ Important:
> An API key requires TLS. The client will fail fast if an API key is provided without TLS.
You can also specify your own certificate authority like this (TLS will be automatically enabled):

```java
UmaDbClient client = UmaDbClient.builder()
.withHost("localhost")
.withPort(50051)
.withCertificateAuthority("server.pem")
.build();
```

For API key-protected servers, use the `withApiKey` when building the client:

```java
UmaDbClient client = UmaDbClient.builder()
.withHost("localhost")
.withPort(50051)
.withApiKey("umadb:example-api-key-123456789")
.build();
```

To specify both CA + API key, simply use the corresponding builder methods:

```java
UmaDbClient client = UmaDbClient.builder()
.withHost("localhost")
.withPort(50051)
.withCertificateAuthority("server.pem")
.withApiKey("umadb:example-api-key-123456789")
.build();
```

### Conditional append (optimistic concurrency)

Expand Down
31 changes: 16 additions & 15 deletions src/main/java/io/umadb/client/UmaDbClientBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ public final class UmaDbClientBuilder {

private String host;
private int port = -1;
private boolean tlsEnabled;
private String caFilePath;
private String apiKey;

Expand Down Expand Up @@ -74,6 +75,16 @@ public UmaDbClientBuilder withPort(int port) {
return this;
}

/**
* Enables TLS with default settings.
*
* @return this builder instance
*/
public UmaDbClientBuilder withTlsEnabled() {
this.tlsEnabled = true;
return this;
}

/**
* Enables TLS using a custom Certificate Authority (CA) certificate.
* <p>
Expand All @@ -84,7 +95,8 @@ public UmaDbClientBuilder withPort(int port) {
* @param caFilePath path to the CA certificate file (PEM format)
* @return this builder instance
*/
public UmaDbClientBuilder withTls(String caFilePath) {
public UmaDbClientBuilder withCertificateAuthority(String caFilePath) {
this.tlsEnabled = true;
this.caFilePath = caFilePath;
return this;
}
Expand All @@ -97,26 +109,14 @@ public UmaDbClientBuilder withTls(String caFilePath) {
* </p>
*
* <p>
* <strong>Note:</strong> TLS must be enabled when using an API key.
* <strong>Note:</strong> TLS is automatically enabled when using an API key.
* </p>
*
* @param apiKey the API key to use for authentication
* @return this builder instance
*/
public UmaDbClientBuilder withApiKey(String apiKey) {
this.apiKey = apiKey;
return this;
}

/**
* Enables both TLS and API key authentication in a single call.
*
* @param caFilePath path to the CA certificate file (PEM format)
* @param apiKey the API key to use for authentication
* @return this builder instance
*/
public UmaDbClientBuilder withTlsAndApiKey(String caFilePath, String apiKey) {
this.caFilePath = caFilePath;
this.tlsEnabled = true;
this.apiKey = apiKey;
return this;
}
Expand All @@ -132,6 +132,7 @@ public UmaDbClient build() {
return new UmaDbClientImpl(
host,
port,
tlsEnabled,
caFilePath,
apiKey
);
Expand Down
37 changes: 29 additions & 8 deletions src/main/java/io/umadb/client/grpc/UmaDbClientImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ public final class UmaDbClientImpl implements UmaDbClient {

private final String host;
private final int port;
private final boolean tlsEnabled;
private final String optionalApiKey;
private final Path optionalCaFilePath;

Expand All @@ -59,11 +60,18 @@ public final class UmaDbClientImpl implements UmaDbClient {
*
* @param host UmaDB server host
* @param port UmaDB server port
* @param tlsEnabled indicates whether an encrypted communication (TLS) is to be used
* @param caFilePath optional path to a CA certificate for TLS
* @param apiKey optional API key (requires TLS)
* @throws IllegalArgumentException if arguments are invalid or insecure
*/
public UmaDbClientImpl(String host, int port, String caFilePath, String apiKey) {
public UmaDbClientImpl(
String host,
int port,
boolean tlsEnabled,
String caFilePath,
String apiKey
) {
if (host == null) {
throw new IllegalArgumentException("host must not be null");
}
Expand All @@ -72,12 +80,17 @@ public UmaDbClientImpl(String host, int port, String caFilePath, String apiKey)
}

// Enforce security: API keys must never be sent over plaintext channels
if (apiKey != null && caFilePath == null) {
throw new IllegalArgumentException("TLS cert file must be defined when using API key");
if (apiKey != null && !tlsEnabled) {
throw new IllegalArgumentException("TLS must be enabled when using API key");
}

if (!tlsEnabled && caFilePath != null) {
throw new IllegalArgumentException("TLS must be enabled when using custom CA");
}

this.host = host;
this.port = port;
this.tlsEnabled = tlsEnabled;
this.optionalApiKey = apiKey;
this.optionalCaFilePath = Optional.ofNullable(caFilePath).map(Path::of).orElse(null);
}
Expand Down Expand Up @@ -127,13 +140,21 @@ private List<ClientInterceptor> resolveClientInterceptors() {
private ChannelCredentials resolveChannelCredentials() throws IOException {
return isTlsEnabled() ?
getTlsChannelCredentials() :
InsecureChannelCredentials.create();
getInsecureChannelCredentials();
}

private ChannelCredentials getTlsChannelCredentials() throws IOException {
return TlsChannelCredentials.newBuilder()
.trustManager(optionalCaFilePath.toFile())
.build();
if (optionalCaFilePath != null) {
return TlsChannelCredentials.newBuilder()
.trustManager(optionalCaFilePath.toFile())
.build();
} else {
return TlsChannelCredentials.create();
}
}

private ChannelCredentials getInsecureChannelCredentials() {
return InsecureChannelCredentials.create();
}

@Override
Expand Down Expand Up @@ -197,7 +218,7 @@ private static Optional<Umadb.ErrorResponse> extractErrorResponseFromMetadata(Me
}

private boolean isTlsEnabled() {
return this.optionalCaFilePath != null;
return this.tlsEnabled;
}

@Override
Expand Down
4 changes: 2 additions & 2 deletions src/test/java/io/umadb/client/UmaDbSecureClientTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ void testSecureConnectionWithProperlyConfiguredClient() {
var client = UmaDbClient.builder()
.withHost(SECURED_UMA_DB_CONTAINER.getHost())
.withPort(SECURED_UMA_DB_CONTAINER.getExposedGrpcPort())
.withTls(CLASS_PATH_TLS_CERT)
.withCertificateAuthority(CLASS_PATH_TLS_CERT)
.withApiKey(TEST_API_KEY)
.build();

Expand Down Expand Up @@ -66,7 +66,7 @@ void testConnectWithSecureClientButNoApiKey() {
var client = UmaDbClient.builder()
.withHost(SECURED_UMA_DB_CONTAINER.getHost())
.withPort(SECURED_UMA_DB_CONTAINER.getExposedGrpcPort())
.withTls(CLASS_PATH_TLS_CERT)
.withCertificateAuthority(CLASS_PATH_TLS_CERT)
.build();

client.connect();
Expand Down
Loading