Packages
rebar3_hex
6.2.0
7.1.0
7.0.11
7.0.10
7.0.9
7.0.8
7.0.7
7.0.6
7.0.5
7.0.4
7.0.3
7.0.2
7.0.1
7.0.0
6.11.9
6.11.8
6.11.7
6.11.6
6.11.5
6.11.4
6.11.3
6.11.2
6.11.1
6.11.0
6.10.3
6.10.2
6.10.1
6.10.0
6.9.6
6.9.5
6.9.4
6.9.3
6.9.2
6.9.1
6.9.0
6.8.0
6.7.0
6.6.0
6.5.0
6.4.0
6.3.0
6.2.0
6.1.0
6.0.0
4.1.0
4.0.0
3.1.0
3.0.0
2.5.1
2.5.0
2.4.0
2.3.0
2.2.0
2.1.0
2.0.0
1.20.0
1.19.0
1.18.0
1.17.0
1.16.0
1.15.0
1.14.0
1.13.0
1.12.0
1.11.0
1.10.0
1.9.1
1.9.0
1.8.1
1.8.0
1.7.2
1.7.1
1.7.0
1.6.3
1.6.1
1.6.0
1.5.0
1.4.0
1.3.0
1.2.0
1.1.0
0.9.0
0.8.0
0.7.0
0.6.0
0.5.1
0.5.0
0.4.0
0.3.0
0.2.0
0.1.0
Hex.pm plugin for rebar3
Current section
Files
Jump to
Current section
Files
src/rebar3_hex_user.erl
-module(rebar3_hex_user).
-export([init/1,
do/1,
format_error/1]).
-export([hex_register/2,
whoami/2,
auth/2,
deauth/2,
reset_password/2,
encrypt_write_key/3,
decrypt_write_key/2,
decrypt_write_key/3]).
-include("rebar3_hex.hrl").
-define(PROVIDER, user).
-define(DEPS, []).
-define(ENDPOINT, "users").
%% ===================================================================
%% Public API
%% ===================================================================
-spec init(rebar_state:t()) -> {ok, rebar_state:t()}.
init(State) ->
Provider = providers:create([{name, ?PROVIDER},
{module, ?MODULE},
{namespace, hex},
{bare, true},
{deps, ?DEPS},
{example, "rebar3 hex user <command>"},
{short_desc, "Hex user tasks"},
{desc, ""},
{opts, [rebar3_hex_utils:repo_opt()]}]),
State1 = rebar_state:add_provider(State, Provider),
{ok, State1}.
-spec do(rebar_state:t()) -> {ok, rebar_state:t()} | {error, string()}.
do(State) ->
Repo = rebar3_hex_utils:repo(State),
case rebar_state:command_args(State) of
["register" | _] ->
hex_register(Repo, State);
["whoami" | _] ->
whoami(Repo, State);
["auth" | _] ->
auth(Repo, State);
["deauth" | _] ->
deauth(Repo, State);
["reset_password" | _] ->
reset_password(Repo, State);
_ ->
throw(?PRV_ERROR(bad_command))
end.
-spec format_error(any()) -> iolist().
format_error({whoami_failure, Reason}) ->
io_lib:format("Fetching currently authenticated user failed: ~ts", [Reason]);
format_error(bad_local_password) ->
"Failure to decrypt write key: bad local password";
format_error({registration_failure, Reason}) ->
io_lib:format("Registration of user failed: ~ts", [Reason]);
format_error({generate_key, Reason}) ->
io_lib:format("Failure generating authentication tokens: ~ts", [Reason]);
format_error(no_match_local_password) ->
"Password confirmation failed. The passwords must match.";
format_error(bad_command) ->
"Command must be one of register, whoami, auth, deauth or reset_password";
format_error(Reason) ->
io_lib:format("~p", [Reason]).
hex_register(Repo, State) ->
ec_talk:say("By registering an account on Hex.pm you accept all our "
"policies and terms of service found at https://hex.pm/policies\n"),
Username = list_to_binary(ec_talk:ask_default("Username:", string, "")),
Email = list_to_binary(ec_talk:ask_default("Email:", string, "")),
case get_password() of
<<"">> ->
error;
Password ->
PasswordConfirm = get_password(confirm),
case Password =:= PasswordConfirm of
true ->
ec_talk:say("Registering..."),
create_user(Username, Email, Password, Repo, State);
false ->
?PRV_ERROR({error, "passwords do not match"})
end
end.
whoami(Repo, State) ->
case maps:get(read_key, Repo, undefined) of
undefined ->
ec_talk:say("Not authenticated as any user currently for this repository");
ReadKey ->
case hex_api_user:me(Repo#{api_key => ReadKey}) of
{ok, {200, _Headers, #{<<"username">> := Username,
<<"email">> := Email}}} ->
ec_talk:say("~ts (~ts)", [Username, Email]),
{ok, State};
{ok, {_Status, _Headers, #{<<"message">> := Message}}} ->
?PRV_ERROR({whoami_failure, Message});
{error, Reason} ->
?PRV_ERROR({whoami_failure, io_lib:format("~p", [Reason])})
end
end.
auth(Repo, State) ->
Username = list_to_binary(ec_talk:ask_default("Username:", string, "")),
Password = get_password(),
ec_talk:say("You have authenticated on Hex using your account password. However, "
"Hex requires you to have a local password that applies only to this machine for security "
"purposes. Please enter it."),
LocalPassword = get_password(<<"Local Password: ">>),
ConfirmLocalPassword = get_password(<<"Local Password (confirm): ">>),
case LocalPassword =:= ConfirmLocalPassword of
true ->
generate_all_keys(Username, Password, LocalPassword, Repo, State);
false ->
throw(?PRV_ERROR(no_match_local_password))
end.
deauth(_Repo, _State) ->
ec_talk:say("Currently not implemented.").
%% ec_talk:say("User `~s` removed from the local machine. "
%% "To authenticate again, run `rebar3 hex user auth` "
%% "or create a new user with `rebar3 hex user register`", [Username]).
reset_password(Repo, State) ->
User = ec_talk:ask_default("Username or Email:", string, ""),
case hex_api_user:reset_password(User, Repo) of
{ok, {201, _Headers, #{<<"email">> := Email}}} ->
ec_talk:say("Email with reset link sent to ~ts", [Email]),
{ok, State};
{ok, {_Status, _Headers, #{<<"message">> := Message}}} ->
?PRV_ERROR({reset_failure, Message});
{error, Reason} ->
?PRV_ERROR({reset_failure, io_lib:format("~p", [Reason])})
end.
%% Internal functions
get_password() ->
rebar3_hex_utils:get_password(<<"Account Password: ">>).
get_password(confirm) ->
rebar3_hex_utils:get_password(<<"Account Password (confirm): ">>).
create_user(Username, Email, Password, Repo, State) ->
case hex_api_user:create(Repo, Username, Password, Email) of
{ok, {201, _Headers, _Body}} ->
ec_talk:say("You are required to confirm your email to access your account, "
"a confirmation email has been sent to ~s", [Email]),
ec_talk:say("Then run `rebar3 hex auth -r ~ts` to create and configure api tokens locally.",
[maps:get(name, Repo)]),
{ok, State};
{ok, {_Status, _Headers, #{<<"errors">> := Errors}}} ->
?PRV_ERROR({registration_failure,
rebar3_hex_utils:pretty_print_errors(Errors)});
{error, Reason} ->
?PRV_ERROR({registration_failure, io_lib:format("~p", [Reason])})
end.
pad(Binary) ->
case byte_size(Binary) of
Size when Size =< 16 ->
<<Binary/binary, 0:((16 - Size) * 8)>>;
Size when Size =< 24 ->
<<Binary/binary, 0:((24 - Size) * 8)>>;
Size when Size =< 32 ->
<<Binary/binary, 0:((32 - Size) * 8)>>;
<<Bin:32/binary, _/binary>> ->
Bin
end.
generate_all_keys(Username, Password, LocalPassword, Repo, State) ->
ec_talk:say("Generating all keys..."),
RepoName = maps:get(name, Repo),
Auth = base64:encode_to_string(<<Username/binary, ":", Password/binary>>),
RepoConfig0 = Repo#{api_key => list_to_binary("Basic " ++ Auth)},
%% write key
WriteKeyName = api_key_name(),
WritePermissions = [#{<<"domain">> => <<"api">>}],
{ok, WriteKey} = generate_key(RepoConfig0, WriteKeyName, WritePermissions),
WriteKeyEncrypted = encrypt_write_key(Username, LocalPassword, WriteKey),
%% read key
RepoConfig1 = Repo#{api_key => WriteKey},
ReadKeyName = api_key_name("read"),
ReadPermissions = [#{<<"domain">> => <<"api">>, <<"resource">> => <<"read">>}],
{ok, ReadKey} = generate_key(RepoConfig1, ReadKeyName, ReadPermissions),
%% repo key
ReposKeyName = repos_key_name(),
ReposPermissions = [#{<<"domain">> => <<"repositories">>}],
{ok, ReposKey} = generate_key(RepoConfig1, ReposKeyName, ReposPermissions),
rebar_hex_repos:update_auth_config(#{RepoName => #{username => Username,
write_key => WriteKeyEncrypted,
read_key => ReadKey,
repos_key => ReposKey}}, State),
{ok, State}.
encrypt_write_key(Username, LocalPassword, WriteKey) ->
AAD = Username,
IV = crypto:strong_rand_bytes(16),
{IV, crypto:block_encrypt(aes_gcm, pad(LocalPassword), IV, {AAD, WriteKey})}.
decrypt_write_key(Username, {IV, {CipherText, CipherTag}}) ->
LocalPassword = get_password(<<"Local Password: ">>),
decrypt_write_key(Username, LocalPassword, {IV, {CipherText, CipherTag}}).
decrypt_write_key(Username, LocalPassword, {IV, {CipherText, CipherTag}}) ->
case crypto:block_decrypt(aes_gcm, pad(LocalPassword), IV, {Username, CipherText, CipherTag}) of
error ->
throw(?PRV_ERROR(bad_local_password));
Result ->
Result
end.
generate_key(RepoConfig, KeyName, Permissions) ->
case hex_api_key:add(RepoConfig, KeyName, Permissions) of
{ok, {201, _Headers, #{<<"secret">> := Secret}}} ->
{ok, Secret};
{ok, {_Status, _Headers, #{<<"message">> := Message}}} ->
?PRV_ERROR({generate_key, Message});
{error, Reason} ->
?PRV_ERROR({generate_key, io_lib:format("~p", [Reason])})
end.
hostname() ->
{ok, Name} = inet:gethostname(),
Name.
api_key_name() ->
list_to_binary(hostname()).
api_key_name(Postfix) ->
list_to_binary([hostname(), "-api-", Postfix]).
repos_key_name() ->
list_to_binary([hostname(), "-repositories"]).