Skip to content

Commit 6110fad

Browse files
committed
Add support for dynamic usernames and passwords.
[closes #613] Signed-off-by: Mark Paluch <[email protected]>
1 parent fc546e4 commit 6110fad

6 files changed

+154
-31
lines changed

src/main/java/io/r2dbc/postgresql/PostgresqlConnectionConfiguration.java

+61-13
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@
3636
import io.r2dbc.postgresql.message.backend.NoticeResponse;
3737
import io.r2dbc.postgresql.util.Assert;
3838
import io.r2dbc.postgresql.util.LogLevel;
39+
import org.reactivestreams.Publisher;
40+
import reactor.core.publisher.Mono;
3941
import reactor.netty.resources.LoopResources;
4042
import reactor.util.annotation.Nullable;
4143

@@ -103,7 +105,7 @@ public final class PostgresqlConnectionConfiguration {
103105

104106
private final Map<String, String> options;
105107

106-
private final CharSequence password;
108+
private final Publisher<CharSequence> password;
107109

108110
private final boolean preferAttachedBuffers;
109111

@@ -123,18 +125,18 @@ public final class PostgresqlConnectionConfiguration {
123125

124126
private final TimeZone timeZone;
125127

126-
private final String username;
128+
private final Publisher<String> username;
127129

128130
private PostgresqlConnectionConfiguration(String applicationName, boolean autodetectExtensions, @Nullable boolean compatibilityMode, @Nullable Duration connectTimeout, @Nullable String database,
129131
LogLevel errorResponseLogLevel,
130132
List<Extension> extensions, ToIntFunction<String> fetchSize, boolean forceBinary, @Nullable Duration lockWaitTimeout,
131133
@Nullable LoopResources loopResources,
132134
@Nullable MultiHostConfiguration multiHostConfiguration,
133-
LogLevel noticeLogLevel, @Nullable Map<String, String> options, @Nullable CharSequence password, boolean preferAttachedBuffers,
135+
LogLevel noticeLogLevel, @Nullable Map<String, String> options, Publisher<CharSequence> password, boolean preferAttachedBuffers,
134136
int preparedStatementCacheQueries, @Nullable String schema,
135137
@Nullable SingleHostConfiguration singleHostConfiguration, SSLConfig sslConfig, @Nullable Duration statementTimeout,
136138
boolean tcpKeepAlive, boolean tcpNoDelay, TimeZone timeZone,
137-
String username) {
139+
Publisher<String> username) {
138140
this.applicationName = Assert.requireNonNull(applicationName, "applicationName must not be null");
139141
this.autodetectExtensions = autodetectExtensions;
140142
this.compatibilityMode = compatibilityMode;
@@ -200,7 +202,7 @@ public String toString() {
200202
", multiHostConfiguration='" + this.multiHostConfiguration + '\'' +
201203
", noticeLogLevel='" + this.noticeLogLevel + '\'' +
202204
", options='" + this.options + '\'' +
203-
", password='" + obfuscate(this.password != null ? this.password.length() : 0) + '\'' +
205+
", password='" + obfuscate(this.password != null ? 4 : 0) + '\'' +
204206
", preferAttachedBuffers=" + this.preferAttachedBuffers +
205207
", singleHostConfiguration=" + this.singleHostConfiguration +
206208
", statementTimeout=" + this.statementTimeout +
@@ -261,8 +263,7 @@ Map<String, String> getOptions() {
261263
return Collections.unmodifiableMap(this.options);
262264
}
263265

264-
@Nullable
265-
CharSequence getPassword() {
266+
Publisher<CharSequence> getPassword() {
266267
return this.password;
267268
}
268269

@@ -290,7 +291,7 @@ SingleHostConfiguration getRequiredSingleHostConfiguration() {
290291
return config;
291292
}
292293

293-
String getUsername() {
294+
Publisher<String> getUsername() {
294295
return this.username;
295296
}
296297

@@ -380,7 +381,7 @@ public static final class Builder {
380381
private Map<String, String> options;
381382

382383
@Nullable
383-
private CharSequence password;
384+
private Publisher<CharSequence> password;
384385

385386
private boolean preferAttachedBuffers = false;
386387

@@ -423,7 +424,7 @@ public static final class Builder {
423424
private LoopResources loopResources = null;
424425

425426
@Nullable
426-
private String username;
427+
private Publisher<String> username;
427428

428429
private Builder() {
429430
}
@@ -743,7 +744,31 @@ public Builder options(Map<String, String> options) {
743744
* @return this {@link Builder}
744745
*/
745746
public Builder password(@Nullable CharSequence password) {
746-
this.password = password;
747+
this.password = Mono.justOrEmpty(password);
748+
return this;
749+
}
750+
751+
/**
752+
* Configure the password publisher. The publisher is used on each authentication attempt.
753+
*
754+
* @param password the password
755+
* @return this {@link Builder}
756+
* @since 1.0.3
757+
*/
758+
public Builder password(Publisher<CharSequence> password) {
759+
this.password = Mono.from(password);
760+
return this;
761+
}
762+
763+
/**
764+
* Configure the password supplier. The supplier is used on each authentication attempt.
765+
*
766+
* @param password the password
767+
* @return this {@link Builder}
768+
* @since 1.0.3
769+
*/
770+
public Builder password(Supplier<CharSequence> password) {
771+
this.password = Mono.fromSupplier(password);
747772
return this;
748773
}
749774

@@ -780,7 +805,6 @@ public Builder preferAttachedBuffers(boolean preferAttachedBuffers) {
780805
*
781806
* @param preparedStatementCacheQueries the preparedStatementCacheQueries
782807
* @return this {@link Builder}
783-
* @throws IllegalArgumentException if {@code username} is {@code null}
784808
* @since 0.8.1
785809
*/
786810
public Builder preparedStatementCacheQueries(int preparedStatementCacheQueries) {
@@ -1023,10 +1047,34 @@ public Builder timeZone(TimeZone timeZone) {
10231047
* @throws IllegalArgumentException if {@code username} is {@code null}
10241048
*/
10251049
public Builder username(String username) {
1050+
this.username = Mono.just(Assert.requireNonNull(username, "username must not be null"));
1051+
return this;
1052+
}
1053+
1054+
/**
1055+
* Configure the username publisher. The publisher is used on each authentication attempt.
1056+
*
1057+
* @param username the username
1058+
* @return this {@link Builder}
1059+
* @throws IllegalArgumentException if {@code username} is {@code null}
1060+
*/
1061+
public Builder username(Publisher<String> username) {
10261062
this.username = Assert.requireNonNull(username, "username must not be null");
10271063
return this;
10281064
}
10291065

1066+
/**
1067+
* Configure the username supplier. The supplier is used on each authentication attempt.
1068+
*
1069+
* @param username the username
1070+
* @return this {@link Builder}
1071+
* @throws IllegalArgumentException if {@code username} is {@code null}
1072+
*/
1073+
public Builder username(Supplier<String> username) {
1074+
this.username = Mono.fromSupplier(Assert.requireNonNull(username, "username must not be null"));
1075+
return this;
1076+
}
1077+
10301078
@Override
10311079
public String toString() {
10321080
return "Builder{" +
@@ -1044,7 +1092,7 @@ public String toString() {
10441092
", multiHostConfiguration='" + this.multiHostConfiguration + '\'' +
10451093
", noticeLogLevel='" + this.noticeLogLevel + '\'' +
10461094
", parameters='" + this.options + '\'' +
1047-
", password='" + obfuscate(this.password != null ? this.password.length() : 0) + '\'' +
1095+
", password='" + obfuscate(this.password != null ? 4 : 0) + '\'' +
10481096
", preparedStatementCacheQueries='" + this.preparedStatementCacheQueries + '\'' +
10491097
", schema='" + this.schema + '\'' +
10501098
", singleHostConfiguration='" + this.singleHostConfiguration + '\'' +

src/main/java/io/r2dbc/postgresql/PostgresqlConnectionFactoryProvider.java

+23-2
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import io.r2dbc.spi.ConnectionFactoryOptions;
2828
import io.r2dbc.spi.ConnectionFactoryProvider;
2929
import io.r2dbc.spi.Option;
30+
import org.reactivestreams.Publisher;
3031
import reactor.netty.resources.LoopResources;
3132

3233
import javax.net.ssl.HostnameVerifier;
@@ -37,6 +38,7 @@
3738
import java.util.Map;
3839
import java.util.TimeZone;
3940
import java.util.function.Function;
41+
import java.util.function.Supplier;
4042

4143
import static io.r2dbc.spi.ConnectionFactoryOptions.CONNECT_TIMEOUT;
4244
import static io.r2dbc.spi.ConnectionFactoryOptions.DATABASE;
@@ -290,6 +292,7 @@ public boolean supports(ConnectionFactoryOptions connectionFactoryOptions) {
290292
* @return this {@link PostgresqlConnectionConfiguration.Builder}
291293
* @throws IllegalArgumentException if {@code options} is {@code null}
292294
*/
295+
@SuppressWarnings("unchecked")
293296
private static PostgresqlConnectionConfiguration.Builder fromConnectionFactoryOptions(ConnectionFactoryOptions options) {
294297

295298
Assert.requireNonNull(options, "connectionFactoryOptions must not be null");
@@ -344,7 +347,6 @@ private static PostgresqlConnectionConfiguration.Builder fromConnectionFactoryOp
344347
mapper.fromTyped(LOOP_RESOURCES).to(builder::loopResources);
345348
mapper.from(NOTICE_LOG_LEVEL).map(it -> OptionMapper.toEnum(it, LogLevel.class)).to(builder::noticeLogLevel);
346349
mapper.from(OPTIONS).map(PostgresqlConnectionFactoryProvider::convertToMap).to(builder::options);
347-
mapper.fromTyped(PASSWORD).to(builder::password);
348350
mapper.from(PORT).map(OptionMapper::toInteger).to(builder::port);
349351
mapper.from(PREFER_ATTACHED_BUFFERS).map(OptionMapper::toBoolean).to(builder::preferAttachedBuffers);
350352
mapper.from(PREPARED_STATEMENT_CACHE_QUERIES).map(OptionMapper::toInteger).to(builder::preparedStatementCacheQueries);
@@ -363,7 +365,26 @@ private static PostgresqlConnectionConfiguration.Builder fromConnectionFactoryOp
363365

364366
return TimeZone.getTimeZone(it.toString());
365367
}).to(builder::timeZone);
366-
builder.username("" + options.getRequiredValue(USER));
368+
369+
Object user = options.getRequiredValue(USER);
370+
Object password = options.getValue(PASSWORD);
371+
372+
if (user instanceof Supplier) {
373+
builder.username((Supplier<String>) user);
374+
} else if (user instanceof Publisher) {
375+
builder.username((Publisher<String>) user);
376+
} else {
377+
builder.username("" + user);
378+
}
379+
if (password != null) {
380+
if (password instanceof Supplier) {
381+
builder.password((Supplier<CharSequence>) password);
382+
} else if (password instanceof Publisher) {
383+
builder.password((Publisher<CharSequence>) password);
384+
} else {
385+
builder.password((CharSequence) password);
386+
}
387+
}
367388

368389
return builder;
369390
}

src/main/java/io/r2dbc/postgresql/SingleHostConnectionFunction.java

+37-8
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import io.r2dbc.postgresql.message.backend.AuthenticationMessage;
2727
import io.r2dbc.postgresql.util.Assert;
2828
import reactor.core.publisher.Mono;
29+
import reactor.util.annotation.Nullable;
2930

3031
import java.net.SocketAddress;
3132

@@ -44,26 +45,54 @@ final class SingleHostConnectionFunction implements ConnectionFunction {
4445
public Mono<Client> connect(SocketAddress endpoint, ConnectionSettings settings) {
4546

4647
return this.upstreamFunction.connect(endpoint, settings)
47-
.delayUntil(client -> StartupMessageFlow
48-
.exchange(this::getAuthenticationHandler, client, this.configuration.getDatabase(), this.configuration.getUsername(),
49-
getParameterProvider(this.configuration, settings))
48+
.delayUntil(client -> getCredentials().flatMapMany(credentials -> StartupMessageFlow
49+
.exchange(auth -> getAuthenticationHandler(auth, credentials), client, this.configuration.getDatabase(), credentials.getUsername(),
50+
getParameterProvider(this.configuration, settings)))
5051
.handle(ExceptionFactory.INSTANCE::handleErrorResponse));
5152
}
5253

5354
private static PostgresStartupParameterProvider getParameterProvider(PostgresqlConnectionConfiguration configuration, ConnectionSettings settings) {
5455
return new PostgresStartupParameterProvider(configuration.getApplicationName(), configuration.getTimeZone(), settings);
5556
}
5657

57-
protected AuthenticationHandler getAuthenticationHandler(AuthenticationMessage message) {
58+
protected AuthenticationHandler getAuthenticationHandler(AuthenticationMessage message, UsernameAndPassword usernameAndPassword) {
5859
if (PasswordAuthenticationHandler.supports(message)) {
59-
CharSequence password = Assert.requireNonNull(this.configuration.getPassword(), "Password must not be null");
60-
return new PasswordAuthenticationHandler(password, this.configuration.getUsername());
60+
CharSequence password = Assert.requireNonNull(usernameAndPassword.getPassword(), "Password must not be null");
61+
return new PasswordAuthenticationHandler(password, usernameAndPassword.getUsername());
6162
} else if (SASLAuthenticationHandler.supports(message)) {
62-
CharSequence password = Assert.requireNonNull(this.configuration.getPassword(), "Password must not be null");
63-
return new SASLAuthenticationHandler(password, this.configuration.getUsername());
63+
CharSequence password = Assert.requireNonNull(usernameAndPassword.getPassword(), "Password must not be null");
64+
return new SASLAuthenticationHandler(password, usernameAndPassword.getUsername());
6465
} else {
6566
throw new IllegalStateException(String.format("Unable to provide AuthenticationHandler capable of handling %s", message));
6667
}
6768
}
6869

70+
Mono<UsernameAndPassword> getCredentials() {
71+
72+
return Mono.zip(Mono.from(this.configuration.getUsername()).single(), Mono.from(this.configuration.getPassword()).singleOptional()).map(it -> {
73+
return new UsernameAndPassword(it.getT1(), it.getT2().orElse(null));
74+
});
75+
}
76+
77+
static class UsernameAndPassword {
78+
79+
final String username;
80+
81+
final @Nullable CharSequence password;
82+
83+
public UsernameAndPassword(String username, @Nullable CharSequence password) {
84+
this.username = username;
85+
this.password = password;
86+
}
87+
88+
public String getUsername() {
89+
return this.username;
90+
}
91+
92+
@Nullable
93+
public CharSequence getPassword() {
94+
return this.password;
95+
}
96+
}
97+
6998
}

src/test/java/io/r2dbc/postgresql/PostgresqlConnectionConfigurationUnitTests.java

+1-7
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ void builderHostAndSocket() {
5353

5454
@Test
5555
void builderNoUsername() {
56-
assertThatIllegalArgumentException().isThrownBy(() -> PostgresqlConnectionConfiguration.builder().username(null))
56+
assertThatIllegalArgumentException().isThrownBy(() -> PostgresqlConnectionConfiguration.builder().username((String) null))
5757
.withMessage("username must not be null");
5858
}
5959

@@ -84,9 +84,7 @@ void configuration() {
8484
.hasFieldOrPropertyWithValue("database", "test-database")
8585
.hasFieldOrPropertyWithValue("singleHostConfiguration.host", "test-host")
8686
.hasFieldOrProperty("options")
87-
.hasFieldOrPropertyWithValue("password", null)
8887
.hasFieldOrPropertyWithValue("singleHostConfiguration.port", 100)
89-
.hasFieldOrPropertyWithValue("username", "test-username")
9088
.hasFieldOrProperty("sslConfig")
9189
.hasFieldOrPropertyWithValue("tcpKeepAlive", true)
9290
.hasFieldOrPropertyWithValue("tcpNoDelay", false)
@@ -116,9 +114,7 @@ void configureStatementAndLockTimeouts() {
116114
.hasFieldOrPropertyWithValue("database", "test-database")
117115
.hasFieldOrPropertyWithValue("singleHostConfiguration.host", "test-host")
118116
.hasFieldOrProperty("options")
119-
.hasFieldOrPropertyWithValue("password", null)
120117
.hasFieldOrPropertyWithValue("singleHostConfiguration.port", 100)
121-
.hasFieldOrPropertyWithValue("username", "test-username")
122118
.hasFieldOrProperty("sslConfig")
123119
.hasFieldOrPropertyWithValue("tcpKeepAlive", true)
124120
.hasFieldOrPropertyWithValue("tcpNoDelay", false)
@@ -160,10 +156,8 @@ void configurationDefaults() {
160156
.hasFieldOrPropertyWithValue("applicationName", "r2dbc-postgresql")
161157
.hasFieldOrPropertyWithValue("database", "test-database")
162158
.hasFieldOrPropertyWithValue("singleHostConfiguration.host", "test-host")
163-
.hasFieldOrPropertyWithValue("password", "test-password")
164159
.hasFieldOrPropertyWithValue("singleHostConfiguration.port", 5432)
165160
.hasFieldOrProperty("options")
166-
.hasFieldOrPropertyWithValue("username", "test-username")
167161
.hasFieldOrProperty("sslConfig")
168162
.hasFieldOrPropertyWithValue("tcpKeepAlive", false)
169163
.hasFieldOrPropertyWithValue("tcpNoDelay", true)

src/test/java/io/r2dbc/postgresql/PostgresqlConnectionFactoryProviderUnitTests.java

+31
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
import io.r2dbc.spi.ConnectionFactoryOptions;
2626
import io.r2dbc.spi.Option;
2727
import org.junit.jupiter.api.Test;
28+
import reactor.core.publisher.Mono;
29+
import reactor.test.StepVerifier;
2830

2931
import java.time.Duration;
3032
import java.util.Arrays;
@@ -33,6 +35,7 @@
3335
import java.util.Map;
3436
import java.util.Objects;
3537
import java.util.TimeZone;
38+
import java.util.function.Supplier;
3639

3740
import static io.r2dbc.postgresql.PostgresqlConnectionFactoryProvider.AUTODETECT_EXTENSIONS;
3841
import static io.r2dbc.postgresql.PostgresqlConnectionFactoryProvider.COMPATIBILITY_MODE;
@@ -617,6 +620,34 @@ void shouldConfigureExtensions() {
617620
assertThat(factory.getConfiguration().getExtensions()).containsExactly(testExtension1, testExtension2);
618621
}
619622

623+
@Test
624+
void supportsUsernameAndPasswordSupplier() {
625+
PostgresqlConnectionFactory factory = this.provider.create(builder()
626+
.option(DRIVER, LEGACY_POSTGRESQL_DRIVER)
627+
.option(HOST, "test-host")
628+
.option(Option.valueOf("password"), (Supplier<String>) () -> "test-password")
629+
.option(Option.valueOf("user"), (Supplier<String>) () -> "test-user")
630+
.option(USER, "test-user")
631+
.build());
632+
633+
StepVerifier.create(factory.getConfiguration().getPassword()).expectNext("test-password").verifyComplete();
634+
StepVerifier.create(factory.getConfiguration().getUsername()).expectNext("test-user").verifyComplete();
635+
}
636+
637+
@Test
638+
void supportsUsernameAndPasswordPublisher() {
639+
PostgresqlConnectionFactory factory = this.provider.create(builder()
640+
.option(DRIVER, LEGACY_POSTGRESQL_DRIVER)
641+
.option(HOST, "test-host")
642+
.option(Option.valueOf("password"), Mono.just("test-password"))
643+
.option(Option.valueOf("user"), Mono.just("test-user"))
644+
.option(USER, "test-user")
645+
.build());
646+
647+
StepVerifier.create(factory.getConfiguration().getPassword()).expectNext("test-password").verifyComplete();
648+
StepVerifier.create(factory.getConfiguration().getUsername()).expectNext("test-user").verifyComplete();
649+
}
650+
620651
private static class TestExtension implements Extension {
621652

622653
private final String name;

0 commit comments

Comments
 (0)