Support Couchbase authentication using client certificates
Closes gh-41520
This commit is contained in:
parent
d56dd741d0
commit
add3d87ea1
|
@ -16,8 +16,16 @@
|
|||
|
||||
package org.springframework.boot.autoconfigure.couchbase;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.KeyStore;
|
||||
|
||||
import javax.net.ssl.TrustManagerFactory;
|
||||
|
||||
import com.couchbase.client.core.env.Authenticator;
|
||||
import com.couchbase.client.core.env.CertificateAuthenticator;
|
||||
import com.couchbase.client.core.env.PasswordAuthenticator;
|
||||
import com.couchbase.client.java.Cluster;
|
||||
import com.couchbase.client.java.ClusterOptions;
|
||||
import com.couchbase.client.java.codec.JacksonJsonSerializer;
|
||||
|
@ -36,16 +44,22 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
|
|||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate;
|
||||
import org.springframework.boot.autoconfigure.couchbase.CouchbaseAutoConfiguration.CouchbaseCondition;
|
||||
import org.springframework.boot.autoconfigure.couchbase.CouchbaseProperties.Authentication.Jks;
|
||||
import org.springframework.boot.autoconfigure.couchbase.CouchbaseProperties.Authentication.Pem;
|
||||
import org.springframework.boot.autoconfigure.couchbase.CouchbaseProperties.Ssl;
|
||||
import org.springframework.boot.autoconfigure.couchbase.CouchbaseProperties.Timeouts;
|
||||
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.boot.io.ApplicationResourceLoader;
|
||||
import org.springframework.boot.ssl.SslBundle;
|
||||
import org.springframework.boot.ssl.SslBundles;
|
||||
import org.springframework.boot.ssl.pem.PemSslStore;
|
||||
import org.springframework.boot.ssl.pem.PemSslStoreDetails;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Conditional;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
|
@ -58,6 +72,7 @@ import org.springframework.util.StringUtils;
|
|||
* @author Moritz Halbritter
|
||||
* @author Andy Wilkinson
|
||||
* @author Phillip Webb
|
||||
* @author Scott Frederick
|
||||
* @since 1.4.0
|
||||
*/
|
||||
@AutoConfiguration(after = JacksonAutoConfiguration.class)
|
||||
|
@ -80,25 +95,51 @@ public class CouchbaseAutoConfiguration {
|
|||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public ClusterEnvironment couchbaseClusterEnvironment(CouchbaseConnectionDetails connectionDetails,
|
||||
public ClusterEnvironment couchbaseClusterEnvironment(
|
||||
ObjectProvider<ClusterEnvironmentBuilderCustomizer> customizers, ObjectProvider<SslBundles> sslBundles) {
|
||||
Builder builder = initializeEnvironmentBuilder(connectionDetails, sslBundles.getIfAvailable());
|
||||
Builder builder = initializeEnvironmentBuilder(sslBundles.getIfAvailable());
|
||||
customizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public Authenticator couchbaseAuthenticator(CouchbaseConnectionDetails connectionDetails) throws IOException {
|
||||
if (connectionDetails.getUsername() != null && connectionDetails.getPassword() != null) {
|
||||
return PasswordAuthenticator.create(connectionDetails.getUsername(), connectionDetails.getPassword());
|
||||
}
|
||||
Pem pem = this.properties.getAuthentication().getPem();
|
||||
if (pem.getCertificates() != null) {
|
||||
PemSslStoreDetails details = new PemSslStoreDetails(null, pem.getCertificates(), pem.getPrivateKey());
|
||||
PemSslStore store = PemSslStore.load(details);
|
||||
return CertificateAuthenticator.fromKey(store.privateKey(), pem.getPrivateKeyPassword(),
|
||||
store.certificates());
|
||||
}
|
||||
Jks jks = this.properties.getAuthentication().getJks();
|
||||
if (jks.getLocation() != null) {
|
||||
Resource resource = new ApplicationResourceLoader().getResource(jks.getLocation());
|
||||
String keystorePassword = jks.getPassword();
|
||||
try (InputStream inputStream = resource.getInputStream()) {
|
||||
KeyStore store = KeyStore.getInstance(KeyStore.getDefaultType());
|
||||
store.load(inputStream, (keystorePassword != null) ? keystorePassword.toCharArray() : null);
|
||||
return CertificateAuthenticator.fromKeyStore(store, keystorePassword);
|
||||
}
|
||||
catch (GeneralSecurityException ex) {
|
||||
throw new IllegalStateException("Error reading Couchbase certificate store", ex);
|
||||
}
|
||||
}
|
||||
throw new IllegalStateException("Couchbase authentication requires username and password, or certificates");
|
||||
}
|
||||
|
||||
@Bean(destroyMethod = "disconnect")
|
||||
@ConditionalOnMissingBean
|
||||
public Cluster couchbaseCluster(ClusterEnvironment couchbaseClusterEnvironment,
|
||||
public Cluster couchbaseCluster(ClusterEnvironment couchbaseClusterEnvironment, Authenticator authenticator,
|
||||
CouchbaseConnectionDetails connectionDetails) {
|
||||
ClusterOptions options = ClusterOptions
|
||||
.clusterOptions(connectionDetails.getUsername(), connectionDetails.getPassword())
|
||||
.environment(couchbaseClusterEnvironment);
|
||||
ClusterOptions options = ClusterOptions.clusterOptions(authenticator).environment(couchbaseClusterEnvironment);
|
||||
return Cluster.connect(connectionDetails.getConnectionString(), options);
|
||||
}
|
||||
|
||||
private ClusterEnvironment.Builder initializeEnvironmentBuilder(CouchbaseConnectionDetails connectionDetails,
|
||||
SslBundles sslBundles) {
|
||||
private ClusterEnvironment.Builder initializeEnvironmentBuilder(SslBundles sslBundles) {
|
||||
ClusterEnvironment.Builder builder = ClusterEnvironment.builder();
|
||||
Timeouts timeouts = this.properties.getEnv().getTimeouts();
|
||||
builder.timeoutConfig((config) -> config.kvTimeout(timeouts.getKeyValue())
|
||||
|
|
|
@ -29,6 +29,7 @@ import org.springframework.util.StringUtils;
|
|||
* @author Yulin Qin
|
||||
* @author Brian Clozel
|
||||
* @author Michael Nitschinger
|
||||
* @author Scott Frederick
|
||||
* @since 1.4.0
|
||||
*/
|
||||
@ConfigurationProperties(prefix = "spring.couchbase")
|
||||
|
@ -49,6 +50,8 @@ public class CouchbaseProperties {
|
|||
*/
|
||||
private String password;
|
||||
|
||||
private final Authentication authentication = new Authentication();
|
||||
|
||||
private final Env env = new Env();
|
||||
|
||||
public String getConnectionString() {
|
||||
|
@ -75,10 +78,116 @@ public class CouchbaseProperties {
|
|||
this.password = password;
|
||||
}
|
||||
|
||||
public Authentication getAuthentication() {
|
||||
return this.authentication;
|
||||
}
|
||||
|
||||
public Env getEnv() {
|
||||
return this.env;
|
||||
}
|
||||
|
||||
public static class Authentication {
|
||||
|
||||
private final Pem pem = new Pem();
|
||||
|
||||
private final Jks jks = new Jks();
|
||||
|
||||
public Pem getPem() {
|
||||
return this.pem;
|
||||
}
|
||||
|
||||
public Jks getJks() {
|
||||
return this.jks;
|
||||
}
|
||||
|
||||
public static class Pem {
|
||||
|
||||
/**
|
||||
* PEM-formatted certificates for certificate-based cluster authentication.
|
||||
*/
|
||||
private String certificates;
|
||||
|
||||
/**
|
||||
* PEM-formatted private key for certificate-based cluster authentication.
|
||||
*/
|
||||
private String privateKey;
|
||||
|
||||
/**
|
||||
* Private key password for certificate-based cluster authentication.
|
||||
*/
|
||||
private String privateKeyPassword;
|
||||
|
||||
public String getCertificates() {
|
||||
return this.certificates;
|
||||
}
|
||||
|
||||
public void setCertificates(String certificates) {
|
||||
this.certificates = certificates;
|
||||
}
|
||||
|
||||
public String getPrivateKey() {
|
||||
return this.privateKey;
|
||||
}
|
||||
|
||||
public void setPrivateKey(String privateKey) {
|
||||
this.privateKey = privateKey;
|
||||
}
|
||||
|
||||
public String getPrivateKeyPassword() {
|
||||
return this.privateKeyPassword;
|
||||
}
|
||||
|
||||
public void setPrivateKeyPassword(String privateKeyPassword) {
|
||||
this.privateKeyPassword = privateKeyPassword;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class Jks {
|
||||
|
||||
/**
|
||||
* Java KeyStore location for certificate-based cluster authentication.
|
||||
*/
|
||||
private String location;
|
||||
|
||||
/**
|
||||
* Java KeyStore password for certificate-based cluster authentication.
|
||||
*/
|
||||
private String password;
|
||||
|
||||
/**
|
||||
* Private key password for certificate-based cluster authentication.
|
||||
*/
|
||||
private String privateKeyPassword;
|
||||
|
||||
public String getLocation() {
|
||||
return this.location;
|
||||
}
|
||||
|
||||
public void setLocation(String location) {
|
||||
this.location = location;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return this.password;
|
||||
}
|
||||
|
||||
public void setPassword(String password) {
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
public String getPrivateKeyPassword() {
|
||||
return this.privateKeyPassword;
|
||||
}
|
||||
|
||||
public void setPrivateKeyPassword(String privateKeyPassword) {
|
||||
this.privateKeyPassword = privateKeyPassword;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class Env {
|
||||
|
||||
private final Io io = new Io();
|
||||
|
|
|
@ -21,7 +21,10 @@ import java.util.HashSet;
|
|||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import com.couchbase.client.core.env.Authenticator;
|
||||
import com.couchbase.client.core.env.CertificateAuthenticator;
|
||||
import com.couchbase.client.core.env.IoConfig;
|
||||
import com.couchbase.client.core.env.PasswordAuthenticator;
|
||||
import com.couchbase.client.core.env.SecurityConfig;
|
||||
import com.couchbase.client.core.env.TimeoutConfig;
|
||||
import com.couchbase.client.java.Cluster;
|
||||
|
@ -54,6 +57,7 @@ import static org.mockito.Mockito.mock;
|
|||
* @author Moritz Halbritter
|
||||
* @author Andy Wilkinson
|
||||
* @author Phillip Webb
|
||||
* @author Scott Frederick
|
||||
*/
|
||||
class CouchbaseAutoConfigurationTests {
|
||||
|
||||
|
@ -63,6 +67,7 @@ class CouchbaseAutoConfigurationTests {
|
|||
@Test
|
||||
void connectionStringIsRequired() {
|
||||
this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(ClusterEnvironment.class)
|
||||
.doesNotHaveBean(Authenticator.class)
|
||||
.doesNotHaveBean(Cluster.class));
|
||||
}
|
||||
|
||||
|
@ -79,6 +84,7 @@ class CouchbaseAutoConfigurationTests {
|
|||
.run((context) -> {
|
||||
assertThat(context).hasSingleBean(ClusterEnvironment.class)
|
||||
.hasSingleBean(Cluster.class)
|
||||
.hasSingleBean(PasswordAuthenticator.class)
|
||||
.hasSingleBean(CouchbaseConnectionDetails.class)
|
||||
.doesNotHaveBean(PropertiesCouchbaseConnectionDetails.class);
|
||||
Cluster cluster = context.getBean(Cluster.class);
|
||||
|
@ -94,19 +100,24 @@ class CouchbaseAutoConfigurationTests {
|
|||
this.contextRunner.withUserConfiguration(CouchbaseTestConfiguration.class)
|
||||
.withPropertyValues("spring.couchbase.connection-string=localhost")
|
||||
.run((context) -> {
|
||||
assertThat(context).hasSingleBean(ClusterEnvironment.class).hasSingleBean(Cluster.class);
|
||||
assertThat(context).hasSingleBean(ClusterEnvironment.class)
|
||||
.hasSingleBean(Authenticator.class)
|
||||
.hasSingleBean(Cluster.class);
|
||||
assertThat(context).doesNotHaveBean("couchbaseAuthenticator");
|
||||
assertThat(context.getBean(Cluster.class))
|
||||
.isSameAs(context.getBean(CouchbaseTestConfiguration.class).couchbaseCluster());
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void connectionDetailsShouldOverrideProperties() {
|
||||
void connectionDetailsOverridesProperties() {
|
||||
this.contextRunner.withBean(CouchbaseConnectionDetails.class, this::couchbaseConnectionDetails)
|
||||
.withPropertyValues("spring.couchbase.connection-string=localhost", "spring.couchbase.username=a-user",
|
||||
"spring.couchbase.password=a-password")
|
||||
.run((context) -> {
|
||||
assertThat(context).hasSingleBean(ClusterEnvironment.class).hasSingleBean(Cluster.class);
|
||||
assertThat(context).hasSingleBean(ClusterEnvironment.class)
|
||||
.hasSingleBean(PasswordAuthenticator.class)
|
||||
.hasSingleBean(Cluster.class);
|
||||
Cluster cluster = context.getBean(Cluster.class);
|
||||
assertThat(cluster.core()).extracting("connectionString.hosts")
|
||||
.asInstanceOf(InstanceOfAssertFactories.LIST)
|
||||
|
@ -243,6 +254,41 @@ class CouchbaseAutoConfigurationTests {
|
|||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void passwordAuthenticationWithUsernameAndPassword() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("spring.couchbase.connection-string=localhost", "spring.couchbase.username=user",
|
||||
"spring.couchbase.password=secret")
|
||||
.run((context) -> assertThat(context).hasSingleBean(PasswordAuthenticator.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void certificateAuthenticationWithPemPrivateKeyAndCertificate() {
|
||||
this.contextRunner.withPropertyValues("spring.couchbase.connection-string=localhost",
|
||||
"spring.couchbase.env.ssl.enabled=true",
|
||||
"spring.couchbase.authentication.pem.private-key=classpath:org/springframework/boot/autoconfigure/ssl/key2.pem",
|
||||
"spring.couchbase.authentication.pem.certificates=classpath:org/springframework/boot/autoconfigure/ssl/key2.crt")
|
||||
.run((context) -> assertThat(context).hasSingleBean(CertificateAuthenticator.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void certificateAuthenticationWithJavaKeyStore() {
|
||||
this.contextRunner.withPropertyValues("spring.couchbase.connection-string=localhost",
|
||||
"spring.couchbase.env.ssl.enabled=true",
|
||||
"spring.couchbase.authentication.jks.location=classpath:org/springframework/boot/autoconfigure/ssl/keystore.jks",
|
||||
"spring.couchbase.authentication.jks.password=secret")
|
||||
.run((context) -> assertThat(context).hasSingleBean(CertificateAuthenticator.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void failsWithMissingAuthentication() {
|
||||
this.contextRunner.withPropertyValues("spring.couchbase.connection-string=localhost").run((context) -> {
|
||||
assertThat(context).hasFailed();
|
||||
assertThat(context).getFailure()
|
||||
.hasMessageContaining("Couchbase authentication requires username and password, or certificates");
|
||||
});
|
||||
}
|
||||
|
||||
private CouchbaseConnectionDetails couchbaseConnectionDetails() {
|
||||
return new CouchbaseConnectionDetails() {
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
package org.springframework.boot.autoconfigure.couchbase;
|
||||
|
||||
import com.couchbase.client.core.env.Authenticator;
|
||||
import com.couchbase.client.java.Cluster;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
@ -27,15 +28,23 @@ import static org.mockito.Mockito.mock;
|
|||
* Test configuration for couchbase that mocks access.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @author Scott Frederick
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
class CouchbaseTestConfiguration {
|
||||
|
||||
private final Cluster cluster = mock(Cluster.class);
|
||||
|
||||
private final Authenticator authenticator = mock(Authenticator.class);
|
||||
|
||||
@Bean
|
||||
Cluster couchbaseCluster() {
|
||||
return this.cluster;
|
||||
}
|
||||
|
||||
@Bean
|
||||
Authenticator couchbaseAuth() {
|
||||
return this.authenticator;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -543,7 +543,7 @@ There are `spring-boot-starter-data-couchbase` and `spring-boot-starter-data-cou
|
|||
|
||||
You can get a `Cluster` by adding the Couchbase SDK and some configuration.
|
||||
The `spring.couchbase.*` properties can be used to customize the connection.
|
||||
Generally, you provide the https://github.com/couchbaselabs/sdk-rfcs/blob/master/rfc/0011-connection-string.md[connection string], username, and password, as shown in the following example:
|
||||
Generally, you provide the https://docs.couchbase.com/dotnet-sdk/current/howtos/managing-connections.html[connection string] and credentials for authentication. Basic authentication with username and password can be configured as shown in the following example:
|
||||
|
||||
[configprops,yaml]
|
||||
----
|
||||
|
@ -554,6 +554,39 @@ spring:
|
|||
password: "secret"
|
||||
----
|
||||
|
||||
https://docs.couchbase.com/server/current/manage/manage-security/configure-client-certificates.html[Client certificates] can be used for authentication instead of username and password.
|
||||
The location and password for a Java KeyStore containing client certificates can be configured as shown in the following example:
|
||||
|
||||
[configprops,yaml]
|
||||
----
|
||||
spring:
|
||||
couchbase:
|
||||
connection-string: "couchbase://192.168.1.123"
|
||||
env:
|
||||
ssl:
|
||||
enabled: true
|
||||
authentication:
|
||||
jks:
|
||||
location: "classpath:client.p12"
|
||||
password: "secret"
|
||||
----
|
||||
|
||||
PEM-encoded certificates and a private key can be configured as shown in the following example:
|
||||
|
||||
[configprops,yaml]
|
||||
----
|
||||
spring:
|
||||
couchbase:
|
||||
connection-string: "couchbase://192.168.1.123"
|
||||
env:
|
||||
ssl:
|
||||
enabled: true
|
||||
authentication:
|
||||
pem:
|
||||
certificates: "classpath:client.crt"
|
||||
private-key: "classpath:client.key"
|
||||
----
|
||||
|
||||
It is also possible to customize some of the `ClusterEnvironment` settings.
|
||||
For instance, the following configuration changes the timeout to open a new `Bucket` and enables SSL support with a reference to a configured xref:features/ssl.adoc[SSL bundle]:
|
||||
|
||||
|
|
Loading…
Reference in New Issue