%% @doc `rebar3 hex user' - Hex user tasks.
%%
%% <h2> Register a user </h2>
%%
%% ```
%% $ rebar3 hex user register
%% '''
%%
%% <h2> Print the current user </h2>
%%
%% ```
%% $ rebar3 hex user whoami
%% '''
%%
%% <h2> Authorize a new user </h2>
%%
%% ```
%% $ rebar3 hex user auth [--key-name KEY_NAME]
%% '''
%%
%% <h2> Deauthorize the user </h2>
%%
%% Deauthorizes the user from the local machine by removing the API key from the Hex config.
%%
%% ```
%% $ rebar3 hex user deauth
%% '''
%%
%% <h2>Generate user key</h2>
%%
%% Generates an unencrypted API key for your account. Keys generated by this command will be owned by you and will
%% give access to your private resources, do not share this key with anyone. For keys that will be shared by
%% organization members use `rebar3 hex organization key' instead. By default this command sets the api:write
%% permission which allows write access to the API, it can be overridden with the --permission flag.
%%
%% ```
%% $ rebar3 hex user key generate [--key-name KEY_NAME] [--permission PERMISSION]
%% '''
%%
%% <h2> Revoke key </h2>
%% Removes given key from account.
%% The key can no longer be used to authenticate API requests.
%%
%% ```
%% $ rebar3 hex user key revoke NAME KEY_NAME
%% '''
%%
%% == Revoke all keys ==
%% Revoke all keys from your account.
%%
%% ```
%% $ rebar3 hex user key revoke --all
%% '''
%%
%% <h2> List keys </h2>
%% Lists all keys associated with the organization.
%%
%% ```
%% $ rebar3 hex user key list
%% '''
%%
%% <h2> Reset user account password </h2>
%%
%% Starts the process for resetting account password.
%%
%% ```
%% rebar3 hex user reset_password account
%% '''
%%
%% <h2> Reset local password </h2>
%%
%% ```
%% rebar3 hex user reset_password local
%% '''
%%
%% <h2> Command line options </h2>
%%
%% <ul>
%% <li>
%% `--repo' - Specify the repository to work with. This option is required when
%% you have multiple repositories configured, including organizations. The argument must be a fully qualified
%% repository name (e.g, `hexpm', `hexpm:my_org', `my_own_hexpm').
%% Default to `hexpm'.
%% </li>
%% <li>
%% `--message "MESSAGE"' - Required message (up to 140 characters) clarifying the retirement reason
%% </li>
%% <li>
%% `--key-name KEY_NAME' - By default Hex will base the key name on your machine's hostname and the organization
%% name, use this option to give your own name.
%% </li>
%% <li>
%% `--permission PERMISSION' - Sets the permissions on the key, this option can be given multiple times,
%% possible values are:
%% <ul>
%% <br/>
%% <li>`api:read' - API read access.</li>
%% <li>`api:write' - API write access.</li>
%% <li>`repository:ORGANIZATION_NAME' - Access to the repository (this is the default permission).</li>
%% <li>`repositories' - Access to repositories for all organizations you are member of.</li>
%% </ul>
%% </li>
%% </ul>
-module(rebar3_hex_user).
-export([init/1,
do/1,
format_error/1]).
-export([encrypt_write_key/3,
decrypt_write_key/2]).
-include("rebar3_hex.hrl").
-define(PROVIDER, user).
-define(DEPS, [{default, lock}]).
%% ===================================================================
%% Public API
%% ===================================================================
%% @private
-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:repo_opt(),
{all, $a, "all", boolean, "all."},
{key_name, $k, "key-name", string, "key-name"},
{permission, $p, "permission", list, "perms."}
]
}]),
State1 = rebar_state:add_provider(State, Provider),
{ok, State1}.
%% @private
-spec do(rebar_state:t()) -> {ok, rebar_state:t()}.
do(State) ->
case rebar3_hex:task_state(State) of
{ok, Task} ->
handle_task(Task);
{error, Reason} ->
?RAISE(Reason)
end.
%% @private
-spec format_error(any()) -> iolist().
format_error({decrypt_write_key, no_write_key}) ->
"No write key found for user in this repository. "
"Be sure you have authenticated first with : rebar3 hex user auth";
format_error({whoami, Reason}) when is_binary(Reason) ->
io_lib:format("Fetching currently authenticated user failed: ~ts", [Reason]);
format_error({input_required, InputName}) ->
Str = io_lib:format("The task you are attempting to run requires a ~ts. ", [InputName]),
Str ++ io_lib:format("Try running this again and be sure to give a ~ts when prompted.", [InputName]);
format_error(bad_local_password) ->
"Failure to decrypt write key: bad local password";
format_error({registration_failure, Errors}) when is_map(Errors) ->
Reason = rebar3_hex_client:pretty_print_errors(Errors),
io_lib:format("Registration of user failed: ~ts", [Reason]);
format_error({generate_key, Reason}) when is_binary(Reason) ->
io_lib:format("Failure generating authentication tokens: ~ts", [Reason]);
format_error({key_revoke, {error, #{<<"status">> := 404}}}) ->
"The key you tried to revoke was not found";
format_error({key_revoke_all, {error, #{<<"message">> := Msg}}}) ->
io_lib:format("Error revoking all keys : ~ts", [Msg]);
format_error({key_list, {error, #{<<"message">> := Msg}}}) ->
io_lib:format("Error listing keys : ~ts", [Msg]);
format_error(passwords_do_not_match) ->
"Password confirmation failed. The passwords must match.";
format_error(local_password_too_big) ->
"Local passwords can not exceed 32 characters.";
format_error({reset_account_password, Reason}) when is_binary(Reason) ->
io_lib:format("Error reseting account password: ~ts", [Reason]);
format_error(not_authenticated) ->
"Not authenticated as any user currently for this repository";
format_error(bad_command) ->
"Invalid arguments, expected one of:\n\n"
"rebar3 hex user register\n"
"rebar3 hex user auth\n"
"rebar3 hex user deauth\n"
"rebar3 hex user whoami\n"
"rebar3 hex key generate\n"
"rebar3 hex key revoke --key-name KEY_NAME\n"
"rebar3 hex key revoke --all\n"
"rebar3 hex key list\n"
"rebar3 hex key fetch --key-name KEY_NAME\n"
"rebar3 hex reset_password account\n"
"rebar3 hex reset_password local\n";
format_error(Reason) ->
rebar3_hex_error:format_error(Reason).
handle_task(#{args := #{task := register}} = Task) ->
#{repo := Repo, state := State} = Task,
rebar3_hex_io: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 = get_string_input("Username"),
Email = get_string_input("Email"),
Password = get_password(account),
rebar3_hex_io:say("Registering..."),
create_user(Username, Email, Password, Repo, State);
handle_task(#{args := #{task := auth}} = Task) ->
#{repo := Repo, state := State} = Task,
Username = get_string_input("Username"),
Password = get_password(account),
Auth = base64:encode_to_string(<<Username/binary, ":", Password/binary>>),
RepoConfig0 = Repo#{api_key => rebar_utils:to_binary("Basic " ++ Auth)},
%% write key
WriteKeyName = api_key_name(),
WritePermissions = [#{<<"domain">> => <<"api">>}],
WriteKey = generate_key(RepoConfig0, WriteKeyName, WritePermissions),
rebar3_hex_io: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),
rebar3_hex_io:say("Generating keys..."),
WriteKeyEncrypted = encrypt_write_key(Username, LocalPassword, WriteKey),
%% read key
RepoConfig1 = Repo#{api_key => WriteKey},
ReadKeyName = api_key_name("read"),
ReadPermissions = [#{<<"domain">> => <<"api">>, <<"resource">> => <<"read">>}],
ReadKey = generate_key(RepoConfig1, ReadKeyName, ReadPermissions),
%% repo key
ReposKeyName = repos_key_name(),
ReposPermissions = [#{<<"domain">> => <<"repositories">>}],
ReposKey = generate_key(RepoConfig1, ReposKeyName, ReposPermissions),
% By default a repositories key is created which gives user access to all repositories
% that they are granted access to server side. For the time being we default
% to hexpm for user auth entries as there is currently no other use case.
rebar3_hex_config:update_auth_config(#{?DEFAULT_HEX_REPO => #{
username => Username,
write_key => WriteKeyEncrypted,
read_key => ReadKey,
repo_key => ReposKey}}, State),
rebar3_hex_io:say("You are now ready to interact with your hex repositories."),
{ok, State};
handle_task(#{args := #{task := deauth}} = Task) ->
#{repo := Repo, state := State} = Task,
case Repo of
#{username := Username, name := RepoName} ->
rebar3_hex_config:update_auth_config(#{RepoName => #{}}, State),
rebar3_hex_io: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]),
{ok, State};
_ ->
rebar3_hex_io:say("Not authenticated as any user currently for this repository"),
{ok, State}
end;
handle_task(#{args := #{task := reset_password, account := true}} = Task) ->
#{repo := Repo, state := State} = Task,
User = get_string_input("Username or Email"),
case rebar3_hex_client:reset_password(Repo, rebar_utils:to_binary(User)) of
{ok, _} ->
rebar3_hex_io:say("Email with reset link sent", []),
{ok, State};
{error, #{<<"message">> := Message}} ->
?RAISE({reset_account_password, Message});
Error ->
?RAISE({reset_account_password, Error})
end;
%% TODO: Write a test
handle_task(#{args := #{task := reset_password, local := true}} = Task) ->
#{repo := Repo, state := State} = Task,
case Repo of
#{username := Username, write_key := EncryptedWriteKey, read_key := ReadKey, repo_key := ReposKey} ->
DecryptedWriteKey = decrypt_write_key(Username, EncryptedWriteKey),
LocalPassword = get_password(new_local),
NewEncryptedWriteKey = encrypt_write_key(Username, LocalPassword, DecryptedWriteKey),
rebar3_hex_config:update_auth_config(#{?DEFAULT_HEX_REPO => #{
username => Username,
write_key => NewEncryptedWriteKey,
read_key => ReadKey,
repo_key => ReposKey}}, State),
{ok, State};
_ ->
rebar3_hex_io:say("Not authenticated as any user currently for this repository"),
{ok, State}
end;
handle_task(#{args := #{task := whoami}, state := State}) ->
Parents = rebar3_hex_config:parent_repos(State),
[whoami(R, State) || R <- Parents],
{ok, State};
handle_task(#{args := #{task := key, generate := true} = Args} = Task) ->
#{raw_opts := Opts, repo := Repo, state := State} = Task,
KeyName = maps:get(key_name, Args, undefined),
Username = get_string_input("Username"),
Password = get_password(account),
Auth = base64:encode_to_string(<<Username/binary, ":", Password/binary>>),
Config = Repo#{api_key => rebar_utils:to_binary("Basic " ++ Auth)},
PermOpts = proplists:get_all_values(permission, Opts),
Perms = rebar3_hex_key:convert_permissions(PermOpts, [#{<<"domain">> => <<"api">>}]),
_ = generate_key(Config, KeyName, Perms),
rebar3_hex_io:say("Key successfully created", []),
{ok, State};
handle_task(#{args := #{task := key, revoke := true, all := true}} = Task) ->
#{repo := Repo, state := State} = Task,
Config = rebar3_hex_config:get_hex_config(?MODULE, Repo, write),
case rebar3_hex_key:revoke_all(Config) of
ok ->
rebar3_hex_io:say("All keys successfully revoked", []),
{ok, State};
Error ->
?RAISE({key_revoke_all, Error})
end;
handle_task(#{args := #{task := key, revoke := true, key_name := KeyName}} = Task) ->
#{repo := Repo, state := State} = Task,
Config = rebar3_hex_config:get_hex_config(?MODULE, Repo, write),
case rebar3_hex_key:revoke(Config, KeyName) of
ok ->
rebar3_hex_io:say("Key successfully revoked", []),
{ok, State};
Error ->
?RAISE({key_revoke, Error})
end;
handle_task(#{repo := Repo, state := State, args := #{task := key, list := true}}) ->
Config = rebar3_hex_config:get_hex_config(?MODULE, Repo, read),
case rebar3_hex_key:list(Config) of
ok ->
{ok, State};
Error ->
?RAISE({key_list, Error})
end;
handle_task(#{args := #{task := key, fetch := true, key_name := KeyName}} = Task) ->
#{repo := Repo, state := State} = Task,
Config = rebar3_hex_config:get_hex_config(?MODULE, Repo, read),
case rebar3_hex_key:fetch(Config, KeyName) of
ok ->
{ok, State};
Error ->
?RAISE({key_list, Error})
end;
handle_task(_) ->
?RAISE(bad_command).
-dialyzer({no_fail_call, whoami/2}).
whoami(#{name := Name} = Repo, State) ->
case maps:get(read_key, Repo, undefined) of
undefined ->
?RAISE(not_authenticated);
ReadKey ->
case rebar3_hex_client:me(maps:remove(name, Repo#{api_key => ReadKey})) of
{ok, #{<<"username">> := Username,
<<"email">> := Email}} ->
rebar3_hex_io:say("~ts : ~ts (~ts)", [Name, Username, Email]),
{ok, State};
{error, #{<<"message">> := Message}} ->
?RAISE({whoami, Message});
Err ->
?RAISE({whoami, Err})
end
end.
get_string_input(Prompt) ->
MaxRetries = 3,
do_get_string_input(Prompt, MaxRetries).
do_get_string_input(Prompt, 0) ->
?RAISE({input_required, Prompt});
do_get_string_input(Prompt, MaxRetries) ->
case rebar3_hex_io:ask(Prompt ++ ":", string, "") of
no_data ->
rebar_api:warn("A ~ts is required for this task, please try again.", [Prompt]),
do_get_string_input(Prompt, MaxRetries - 1);
Username ->
rebar_utils:to_binary(Username)
end.
local_password_check(Pw) ->
byte_size(Pw) < 32 orelse "Local passwords can not be greater than 32 characters, please try again".
get_password(account) ->
get_password(<<"Account">>, fun(_) -> true end);
get_password(new_local) ->
get_password(<<"New local">>, fun(Pw) -> local_password_check(Pw) end);
get_password(local) ->
get_password(<<"Local">>, fun(Pw) -> local_password_check(Pw) end).
get_password(Type, Fun) when is_binary(Type) ->
MaxRetries = 3,
do_get_password(Type, Fun, MaxRetries).
do_get_password(_, _, 0) ->
?RAISE(passwords_do_not_match);
do_get_password(Type, Fun, MaxRetries) ->
Rest = <<" Password: ">>,
Password = rebar3_hex_io:get_password(<<Type/binary, Rest/binary>>),
case Fun(Password) of
true ->
confirm_password(Type, Fun, Password, MaxRetries);
Err ->
rebar_api:warn(Err, []),
do_get_password(Type, Fun, MaxRetries - 1)
end.
confirm_password(Type, Fun, ExpectedPw, MaxRetries) ->
Rest = <<" Password (confirm): ">>,
case rebar3_hex_io:get_password(<<Type/binary, Rest/binary>>) of
Pw when Pw =:= ExpectedPw ->
Pw;
_ ->
rebar_api:warn("Passwords do not match, please try again", []),
do_get_password(Type, Fun, MaxRetries - 1)
end.
-dialyzer({no_fail_call, create_user/5}).
create_user(Username, Email, Password, Repo, State) ->
case rebar3_hex_client:create_user(Repo, Username, Password, Email) of
{ok, _} ->
rebar3_hex_io:say("You are required to confirm your email to access your account, "
"a confirmation email has been sent to ~s", [Email]),
rebar3_hex_io:say("Then run `rebar3 hex auth -r ~ts` to create and configure api tokens locally.",
[maps:get(repo_name, Repo)]),
{ok, State};
{error, #{<<"errors">> := Errors}} ->
?RAISE({registration_failure, Errors});
Error ->
?RAISE({registration_failure, Error})
end.
%% @private
encrypt_write_key(Username, LocalPassword, WriteKey) ->
rebar3_hex_config:encrypt_write_key(Username, LocalPassword, WriteKey).
%% @private
%-spec decrypt_write_key(binary(), {binary(), {binary(), binary()}} | undefined) -> binary().
decrypt_write_key(_Username, undefined) ->
{decrypt_write_key, no_write_key};
decrypt_write_key(Username, Key) ->
MaxRetries = 2,
LocalPassword = rebar3_hex_io:get_password(<<"Local Password: ">>),
decrypt_write_key(Username, LocalPassword, Key, MaxRetries).
-ifdef(POST_OTP_22).
decrypt_write_key(_, _, _, 0) ->
?RAISE(bad_local_password);
decrypt_write_key(Username, LocalPassword, Key, MaxRetries) ->
case rebar3_hex_config:decrypt_write_key(Username, LocalPassword, Key) of
error ->
rebar_api:warn("Sorry, try again.", []),
LocalPassword1 = rebar3_hex_io:get_password(<<"Local Password: ">>),
decrypt_write_key(Username, LocalPassword1, Key, MaxRetries - 1);
Result ->
Result
end.
-else.
decrypt_write_key(_, _, _, 0) ->
?RAISE(bad_local_password);
decrypt_write_key(Username, LocalPassword, Key, MaxRetries) ->
case rebar3_hex_config:decrypt_write_key(Username, LocalPassword, Key) of
error ->
rebar_api:warn("Sorry, try again.", []),
LocalPassword1 = rebar3_hex_io:get_password(<<"Local Password: ">>),
decrypt_write_key(Username, LocalPassword1, Key, MaxRetries - 1);
Result ->
Result
end.
-endif.
-dialyzer({nowarn_function, generate_key/3}).
%%-dialyzer({nowarn_function, hex_api_key:add/3}).
generate_key(HexConfig, KeyName, Perms) ->
case rebar3_hex_key:generate(HexConfig, KeyName, Perms) of
{ok, #{<<"secret">> := Secret}} ->
Secret;
{error, #{<<"message">> := Message}} ->
?RAISE({generate_key, Message});
Error ->
?RAISE({generate_key, Error})
end.
hostname() ->
{ok, Name} = inet:gethostname(),
Name.
api_key_name() ->
rebar_utils:to_binary(hostname()).
-dialyzer({nowarn_function, api_key_name/1}).
api_key_name(Postfix) ->
rebar_utils:to_binary([hostname(), "-api-", Postfix]).
-dialyzer({nowarn_function, repos_key_name/0}).
repos_key_name() ->
rebar_utils:to_binary([hostname(), "-repositories"]).