Packages
rebar3_hex
7.0.2
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
%% @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"]).