Skip to content

Commit eff43e9

Browse files
authored
feat: support function as password for dynamic credentials (#122)
* feat: support function as password for dynamic credentials Accept fun(() -> iodata()) as the password config value. The function is evaluated on each new connection, enabling rotating credentials like AWS IAM auth tokens. When stored in pool config, the fun prints as #Fun<...> in logs, preventing accidental password disclosure. Closes #56 Closes #117 * fix: remove duplicated lines from merge conflict resolution
1 parent 0521dba commit eff43e9

3 files changed

Lines changed: 26 additions & 5 deletions

File tree

src/pgo.erl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@
6060
-type pool_config() :: #{host => string(),
6161
port => integer(),
6262
user => string(),
63-
password => string(),
63+
password => string() | fun(() -> iodata()),
6464
database => string(),
6565

6666
%% pool specific settings

src/pgo_handler.erl

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@
1818
-define(DEFAULT_USER, "postgres").
1919
-define(DEFAULT_PASSWORD, "").
2020

21+
resolve_password(Options) ->
22+
case maps:get(password, Options, ?DEFAULT_PASSWORD) of
23+
Fun when is_function(Fun, 0) -> Fun();
24+
Password -> Password
25+
end.
26+
2127
% driver options.
2228
%% -type open_option() ::
2329
%% {host, inet:ip_address() | inet:hostname()} % default: ?DEFAULT_HOST
@@ -229,7 +235,7 @@ setup_startup(Conn=#conn{socket_module=SocketModule,
229235
end.
230236

231237
setup_authenticate_cleartext_password(Conn, Options) ->
232-
Password = maps:get(password, Options, ?DEFAULT_PASSWORD),
238+
Password = resolve_password(Options),
233239
setup_authenticate_password(Conn, Password).
234240

235241
setup_authenticate_sasl_password(Conn, MethodsBinary, Options) ->
@@ -291,7 +297,7 @@ scram_client_final(Nonce, ServerFirst, #conn{socket_module=SocketModule,
291297
pool=Pool}, Options) ->
292298
User = maps:get(user, Options, ?DEFAULT_USER),
293299
ServerFirstParts = pgo_scram:parse_server_first(ServerFirst, Nonce),
294-
Password = maps:get(password, Options, ?DEFAULT_PASSWORD),
300+
Password = resolve_password(Options),
295301
{ClientFinalMessage, ServerProof} = pgo_scram:get_client_final(ServerFirstParts, Nonce, User, Password),
296302
case SocketModule:send(Socket, pgo_protocol:encode_scram_response_message(ClientFinalMessage)) of
297303
ok ->
@@ -302,7 +308,7 @@ scram_client_final(Nonce, ServerFirst, #conn{socket_module=SocketModule,
302308

303309
setup_authenticate_md5_password(Conn, Salt, Options) ->
304310
User = maps:get(user, Options, ?DEFAULT_USER),
305-
Password = maps:get(password, Options, ?DEFAULT_PASSWORD),
311+
Password = resolve_password(Options),
306312
% concat('md5', md5(concat(md5(concat(password, username)), random-salt)))
307313
<<MD51Int:128>> = crypto:hash(md5, [Password, User]),
308314
MD51Hex = io_lib:format("~32.16.0b", [MD51Int]),

test/pgo_SUITE.erl

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,24 @@
1616

1717
all() -> [checkout_checkin, checkout_break, recheckout, kill_socket, kill_pid,
1818
checkout_kill, checkout_disconnect, checkout_query_crash,
19-
query_timeout].
19+
password_as_function, query_timeout].
2020

2121
init_per_suite(Config) ->
2222
Config.
2323

2424
end_per_suite(_Config) ->
2525
ok.
2626

27+
init_per_testcase(password_as_function, Config) ->
28+
Pool = pool_password_as_function,
29+
application:ensure_all_started(pgo),
30+
pgo_sup:start_child(Pool, #{pool_size => 1,
31+
database => ?DATABASE,
32+
user => ?USER,
33+
password => fun() -> ?PASSWORD end}),
34+
Tid = pgo_pool:tid(Pool),
35+
?UNTIL((catch ets:info(Tid, size)) =:= 1),
36+
[{pool_name, Pool} | Config];
2737
init_per_testcase(T, Config) when T =:= checkout_break ;
2838
T =:= checkout_query_crash ;
2939
T =:= recheckout ;
@@ -225,6 +235,11 @@ checkout_disconnect(Config) ->
225235

226236
ok.
227237

238+
password_as_function(Config) ->
239+
Name = ?config(pool_name, Config),
240+
?assertMatch(#{rows := [{1}]}, pgo:query("select 1", [], #{pool => Name})),
241+
ok.
242+
228243
%% regression test. this would fail with `unexpected message` response to the create query
229244
checkout_query_crash(Config) ->
230245
Name = ?config(pool_name, Config),

0 commit comments

Comments
 (0)