From 803d863c5643b9356269c2e4963e52e2dc816e76 Mon Sep 17 00:00:00 2001 From: Holger Weiss Date: Tue, 1 Aug 2023 21:26:13 +0200 Subject: [PATCH] Support static credentials Allow for configuring a map of static username/password pairs using the new 'credentials' option. TODO: Documentation (doc/overview.edoc and CHANGELOG.md). --- src/eturnal.erl | 44 +++++++++++++++------ src/eturnal_ctl.erl | 35 +++++----------- src/eturnal_yaml.erl | 2 + test/eturnal_SUITE.erl | 4 +- test/eturnal_SUITE_data/eturnal-new-otp.yml | 2 + test/eturnal_SUITE_data/eturnal-old-otp.yml | 2 + 6 files changed, 51 insertions(+), 38 deletions(-) diff --git a/src/eturnal.erl b/src/eturnal.erl index a83f4362e..b16bb2fb3 100644 --- a/src/eturnal.erl +++ b/src/eturnal.erl @@ -138,12 +138,17 @@ handle_call({set_loglevel, Level}, _From, State) -> {reply, Err, State} end; handle_call({get_password, Username}, _From, State) -> - case get_opt(secret) of - [Secret | _Secrets] -> + case {get_opt(secret), is_dynamic_username(Username)} of + {[Secret | _Secrets], true} -> Password = derive_password(Username, [Secret]), {reply, {ok, Password}, State}; - undefined -> - {reply, {error, no_secret}, State} + {_, _} -> + case maps:get(Username, get_opt(credentials), undefined) of + Password when is_binary(Password) -> + {reply, {ok, Password}, State}; + undefined -> + {reply, {error, no_credentials}, State} + end end; handle_call(Request, From, State) -> ?LOG_ERROR("Got unexpected request from ~p: ~p", [From, Request]), @@ -235,7 +240,7 @@ get_password(Username, _Realm) -> ExpireTime -> case erlang:system_time(second) of Now when Now < ExpireTime -> - ?LOG_DEBUG("Looking up password for: ~ts", [Username]), + ?LOG_DEBUG("Deriving password for: ~ts", [Username]), derive_password(Username, get_opt(secret)); Now when Now >= ExpireTime -> case get_opt(strict_expiry) of @@ -249,8 +254,14 @@ get_password(Username, _Realm) -> end end catch _:badarg -> - ?LOG_INFO("Non-numeric expiration field: ~ts", [Username]), - <<>> + ?LOG_DEBUG("Looking up password for: ~ts", [Username]), + case maps:get(Username, get_opt(credentials), undefined) of + Password when is_binary(Password) -> + Password; + undefined -> + ?LOG_INFO("Have no password for: ~ts", [Username]), + <<>> + end end. %% API: retrieve option value. @@ -301,6 +312,17 @@ reload_config() -> %% Internal functions: authentication. +-spec is_dynamic_username(binary()) -> boolean(). +is_dynamic_username(Username) -> + case string:to_integer(Username) of + {N, <<":", _Rest/binary>>} when is_integer(N), N > 0 -> + true; + {N, <<>>} when is_integer(N), N > 0 -> + true; + {_, _} -> + false + end. + -spec derive_password(binary(), [binary()]) -> binary() | [binary()]. -ifdef(old_crypto). derive_password(Username, [Secret]) -> @@ -451,7 +473,7 @@ opt_filter(Opt) -> -spec turn_opts(boolean()) -> proplists:proplist(). turn_opts(EnableTURN) -> - case {EnableTURN, got_secret(), got_relay_addr()} of + case {EnableTURN, got_credentials(), got_relay_addr()} of {true, true, true} -> [{use_turn, true}, {auth_type, user}]; @@ -509,8 +531,8 @@ turn_enabled() -> EnableTURN =:= true end, get_opt(listen)). --spec got_secret() -> boolean(). -got_secret() -> +-spec got_credentials() -> boolean(). +got_credentials() -> case get_opt(secret) of Secrets when is_list(Secrets) -> lists:all(fun(Secret) -> @@ -519,7 +541,7 @@ got_secret() -> Secret when is_binary(Secret), byte_size(Secret) > 0 -> true; undefined -> - false + map_size(get_opt(credentials)) > 0 end. -spec got_relay_addr() -> boolean(). diff --git a/src/eturnal_ctl.erl b/src/eturnal_ctl.erl index 61fb861b1..e40e0812b 100644 --- a/src/eturnal_ctl.erl +++ b/src/eturnal_ctl.erl @@ -63,8 +63,8 @@ get_credentials(Expiry, Suffix) -> {ok, Password} -> Credentials = format_credentials(Username, Password), {ok, unicode:characters_to_list(Credentials)}; - {error, no_secret} -> - {error, "No shared secret"}; + {error, no_credentials} -> + {error, "No shared secret and no credentials"}; {error, timeout} -> {error, "Querying eturnal timed out"} end @@ -78,19 +78,13 @@ get_password(Username0) -> ?LOG_DEBUG("Handling API call: get_password(~p)", [Username0]), try unicode:characters_to_binary(Username0) of Username when is_binary(Username) -> - case is_valid_username(Username) of - true -> - case call({get_password, Username}) of - {ok, Password} -> - {ok, unicode:characters_to_list(Password)}; - {error, no_secret} -> - {error, "No shared secret"}; - {error, timeout} -> - {error, "Querying eturnal timed out"} - end; - false -> - ?LOG_DEBUG("Invalid user name: ~s", [Username]), - {error, "Invalid user name: " ++ Username0} + case call({get_password, Username}) of + {ok, Password} -> + {ok, unicode:characters_to_list(Password)}; + {error, no_credentials} -> + {error, "No shared secret and no credentials"}; + {error, timeout} -> + {error, "Querying eturnal timed out"} end; {_, _, _} -> ?LOG_DEBUG("Cannot convert user name to binary: ~p", [Username0]), @@ -208,17 +202,6 @@ reload() -> %% Internal functions. --spec is_valid_username(binary()) -> boolean(). -is_valid_username(Username) -> - case string:to_integer(Username) of - {N, <<":", _Rest/binary>>} when is_integer(N), N > 0 -> - true; - {N, <<>>} when is_integer(N), N > 0 -> - true; - {_, _} -> - false - end. - -spec make_username(string(), string()) -> binary(). make_username(Expiry0, Suffix) -> Expiry = try string:trim(Expiry0) of diff --git a/src/eturnal_yaml.erl b/src/eturnal_yaml.erl index ee47ed108..d378ac0bf 100644 --- a/src/eturnal_yaml.erl +++ b/src/eturnal_yaml.erl @@ -79,6 +79,7 @@ validator() -> blacklist => blacklist_validator(), whitelist => list_or_single(ip_mask()), strict_expiry => bool(), + credentials => map(binary(), binary(), [unique, {return, map}]), realm => non_empty(binary()), software_name => non_empty(binary()), run_dir => directory(write), @@ -107,6 +108,7 @@ validator() -> blacklist => ?DEFAULT_BLACKLIST, whitelist => [], strict_expiry => false, + credentials => #{}, realm => <<"eturnal.net">>, secret => [get_default(secret, make_random_secret())], software_name => <<"eturnal">>, diff --git a/test/eturnal_SUITE.erl b/test/eturnal_SUITE.erl index a72c075d7..494b8df3d 100644 --- a/test/eturnal_SUITE.erl +++ b/test/eturnal_SUITE.erl @@ -160,7 +160,9 @@ check_credentials(_Config) -> {error, _Reason2} = eturnal_ctl:get_credentials(Invalid, []) end, ["Invalid", invalid, [invalid]]), ct:pal("Checking invalid suffix"), - {error, _Reason} = eturnal_ctl:get_credentials(Username, invalid). + {error, _Reason} = eturnal_ctl:get_credentials(Username, invalid), + ct:pal("Checking static credentials"), + {ok, "l0vesBob"} = eturnal_ctl:get_password("alice"). -spec check_loglevel(config()) -> any(). check_loglevel(_Config) -> diff --git a/test/eturnal_SUITE_data/eturnal-new-otp.yml b/test/eturnal_SUITE_data/eturnal-new-otp.yml index e0e9c1eb4..705df83e4 100644 --- a/test/eturnal_SUITE_data/eturnal-new-otp.yml +++ b/test/eturnal_SUITE_data/eturnal-new-otp.yml @@ -21,6 +21,8 @@ eturnal: enable_turn: false relay_ipv4_addr: "127.0.0.1" secret: "crypt1c" + credentials: + alice: l0vesBob log_level: debug tls_options: - no_tlsv1 diff --git a/test/eturnal_SUITE_data/eturnal-old-otp.yml b/test/eturnal_SUITE_data/eturnal-old-otp.yml index 12303c3a8..77f5d0c81 100644 --- a/test/eturnal_SUITE_data/eturnal-old-otp.yml +++ b/test/eturnal_SUITE_data/eturnal-old-otp.yml @@ -16,6 +16,8 @@ eturnal: enable_turn: false relay_ipv4_addr: "127.0.0.1" secret: "crypt1c" + credentials: + alice: l0vesBob log_level: debug tls_options: - no_tlsv1