From 977f5ddd0fd69e050fb1717597833e9970629a5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonatan=20M=C3=A4nnchen?= Date: Thu, 16 Apr 2026 13:42:08 +0200 Subject: [PATCH] Upgrade rebar and utilitze hex_cli_auth --- rebar.config | 3 +- rebar.lock | 7 +- src/rebar3_hex.erl | 39 +- src/rebar3_hex_config.erl | 175 +------- src/rebar3_hex_error.erl | 10 +- src/rebar3_hex_httpc_adapter.erl | 46 +- src/rebar3_hex_organization.erl | 75 ++-- src/rebar3_hex_owner.erl | 38 +- src/rebar3_hex_publish.erl | 54 +-- src/rebar3_hex_repo.erl | 591 +++++++++++++++++++++++++ src/rebar3_hex_retire.erl | 7 +- src/rebar3_hex_search.erl | 5 +- src/rebar3_hex_user.erl | 338 ++++++-------- test/rebar3_hex_config_SUITE.erl | 15 +- test/rebar3_hex_integration_SUITE.erl | 352 ++++++++++++--- test/rebar3_hex_organization_SUITE.erl | 107 +++++ test/rebar3_hex_publish_SUITE.erl | 4 - test/rebar3_hex_repo_SUITE.erl | 84 ++++ test/rebar3_hex_user_SUITE.erl | 91 ++-- test/support/hex_api_model.erl | 103 ++++- test/support/hex_db.erl | 13 +- test/support/test_utils.erl | 13 +- 22 files changed, 1580 insertions(+), 590 deletions(-) create mode 100644 src/rebar3_hex_repo.erl create mode 100644 test/rebar3_hex_organization_SUITE.erl create mode 100644 test/rebar3_hex_repo_SUITE.erl diff --git a/rebar.config b/rebar.config index ac883af0..bd5603cf 100644 --- a/rebar.config +++ b/rebar.config @@ -15,7 +15,8 @@ {project_plugins, [{covertool, "2.0.6"}, {rebar3_ex_doc, "0.2.30"}, {rebar3_hank, "1.4.0"}]}. -{deps, [{hex_core, "0.12.2"}, {verl, "1.1.1"}]}. +%% TODO: Use released hex_core version +{deps, [{hex_core, {git, "https://github.com/maennchen/hex_core.git", {branch, "jm/hex_auth"}}}, {verl, "1.1.1"}]}. {profiles, [ {test, [ diff --git a/rebar.lock b/rebar.lock index dd00cddc..f81edc52 100644 --- a/rebar.lock +++ b/rebar.lock @@ -1,11 +1,12 @@ {"1.2.0", -[{<<"hex_core">>,{pkg,<<"hex_core">>,<<"0.12.2">>},0}, +[{<<"hex_core">>, + {git,"https://github.com/maennchen/hex_core.git", + {ref,"b8cea90a9e2e497e9d8553c81b06939d78cf076d"}}, + 0}, {<<"verl">>,{pkg,<<"verl">>,<<"1.1.1">>},0}]}. [ {pkg_hash,[ - {<<"hex_core">>, <<"116F75685E707624BA79B6F6B06BE4F5E14C5FCCECCF31D4EA3ED4FED6134F8C">>}, {<<"verl">>, <<"98F3EC48B943AA4AE8E29742DE86A7CD752513687911FE07D2E00ECDF3107E45">>}]}, {pkg_hash_ext,[ - {<<"hex_core">>, <<"00E402C302BF4B8CEDC2D3425E22B55F8C70A2542D69C87A2E5BA7AF2745F6C6">>}, {<<"verl">>, <<"0925E51CD92A0A8BE271765B02430B2E2CFF8AC30EF24D123BD0D58511E8FB18">>}]} ]. diff --git a/src/rebar3_hex.erl b/src/rebar3_hex.erl index 3f7712b2..a98921b7 100644 --- a/src/rebar3_hex.erl +++ b/src/rebar3_hex.erl @@ -26,14 +26,37 @@ -export_type([task/0]). init(State) -> - lists:foldl(fun provider_init/2, {ok, State}, [rebar3_hex_user, - rebar3_hex_build, - rebar3_hex_cut, - rebar3_hex_owner, - rebar3_hex_organization, - rebar3_hex_search, - rebar3_hex_retire, - rebar3_hex_publish]). + State1 = override_http_adapters(State), + lists:foldl(fun provider_init/2, {ok, State1}, [rebar3_hex_user, + rebar3_hex_build, + rebar3_hex_cut, + rebar3_hex_owner, + rebar3_hex_organization, + rebar3_hex_repo, + rebar3_hex_search, + rebar3_hex_retire, + rebar3_hex_publish]). + +%% Override http_adapter for all repos to use rebar3_hex_httpc_adapter +override_http_adapters(State) -> + Resources = rebar_state:resources(State), + Resources1 = lists:map(fun(Resource) -> + %% Resource is a #resource{} record: {resource, Type, Module, State, Impl} + case element(2, Resource) of + pkg -> + PkgState = element(4, Resource), + case maps:find(repos, PkgState) of + {ok, Repos} -> + Repos1 = [rebar3_hex_config:set_http_adapter(R) || R <- Repos], + setelement(4, Resource, PkgState#{repos := Repos1}); + error -> + Resource + end; + _ -> + Resource + end + end, Resources), + rebar_state:set_resources(State, Resources1). provider_init(Module, {ok, State}) -> Module:init(State). diff --git a/src/rebar3_hex_config.erl b/src/rebar3_hex_config.erl index 760a0663..81b988dc 100644 --- a/src/rebar3_hex_config.erl +++ b/src/rebar3_hex_config.erl @@ -4,16 +4,17 @@ -export([ api_key_name/1 , api_key_name/2 , all_repos/1 - , encrypt_write_key/3 - , decrypt_write_key/3 , repos_key_name/0 , org_key_name/2 , parent_repos/1 - , get_hex_config/3 , default_repo/1 , repo/1 , repo/2 + , set_http_adapter/1 , update_auth_config/2 + , auth_config/1 + , update_repo_auth_config/3 + , remove_from_auth_config/2 ]). -include("rebar3_hex.hrl"). @@ -31,46 +32,6 @@ api_key_name(Key, Suffix) -> Prefix = key_name_prefix(Key), key_name(Prefix, <<"-api-">>, Suffix). --ifdef(POST_OTP_22). --spec encrypt_write_key(binary(), binary(), binary()) -> {binary(), {binary(), binary()}}. -encrypt_write_key(Username, LocalPassword, WriteKey) -> - AAD = Username, - IV = crypto:strong_rand_bytes(16), - Key = pad(LocalPassword), - {IV, crypto:crypto_one_time_aead(cipher(Key), Key, IV, WriteKey, AAD, true)}. --else. --spec encrypt_write_key(binary(), binary(), binary()) -> {binary(), {binary(), binary()}}. -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})}. --endif. - --ifdef(POST_OTP_22). -decrypt_write_key(Username, LocalPassword, {IV, {CipherText, CipherTag}}) -> - Key = pad(LocalPassword), - crypto:crypto_one_time_aead(cipher(Key), Key, IV, CipherText, Username, CipherTag, false). --else. -decrypt_write_key(Username, LocalPassword, {IV, {CipherText, CipherTag}}) -> - crypto:block_decrypt(aes_gcm, pad(LocalPassword), IV, {Username, CipherText, CipherTag}). --endif. - --ifdef(POST_OTP_22). -cipher(Key) when byte_size(Key) == 16 -> aes_128_gcm; -cipher(Key) when byte_size(Key) == 24 -> aes_192_gcm; -cipher(Key) when byte_size(Key) == 32 -> aes_256_gcm. --endif. - -pad(Binary) -> - case byte_size(Binary) of - Size when Size =< 16 -> - <>; - Size when Size =< 24 -> - <>; - Size when Size =< 32 -> - <> - end. - -spec repos_key_name() -> binary(). repos_key_name() -> key_name(hostname(), <<"-repositories">>). @@ -97,6 +58,15 @@ key_name_prefix(Key) -> Key. update_auth_config(Config, State) -> rebar_hex_repos:update_auth_config(Config, State). +auth_config(State) -> + rebar_hex_repos:auth_config(State). + +update_repo_auth_config(RepoConfig, RepoName, State) -> + rebar_hex_repos:update_repo_auth_config(RepoConfig, RepoName, State). + +remove_from_auth_config(RepoName, State) -> + rebar_hex_repos:remove_from_auth_config(RepoName, State). + all_repos(State) -> Resources = rebar_state:resources(State), #{repos := Repos} = rebar_resource_v2:find_resource_state(pkg, Resources), @@ -111,12 +81,7 @@ repo(State) -> Res = [R || R <- Repos, maps:get(name, R) =/= ?DEFAULT_HEX_REPO], case Res of [] -> - case rebar_hex_repos:get_repo_config(?DEFAULT_HEX_REPO, Repos) of - {ok, Repo} -> - {ok, set_http_adapter(Repo)}; - _ -> - {error, no_repo_in_state} - end; + repo(State, ?DEFAULT_HEX_REPO); [_Repo|_Rest] -> {error, {required, repo}} end; @@ -126,67 +91,17 @@ repo(State) -> repo(State, RepoName) -> BinName = rebar_utils:to_binary(RepoName), - Repos = all_repos(State), - MaybeFound1 = get_repo(BinName, all_repos(State)), - MaybeParentRepo = <<"hexpm:">>, - MaybeFound2 = get_repo(<>, Repos), - case {MaybeFound1, MaybeFound2} of - {{ok, Repo1}, undefined} -> - Repo2 = set_http_adapter(merge_with_env(Repo1)), - Repo3 = maybe_set_api_organization(Repo2), - {ok, maybe_set_api_repository(Repo3)}; - {undefined, {ok, Repo2}} -> - Repo3 = set_http_adapter(merge_with_env(Repo2)), - Repo4 = maybe_set_api_organization(Repo3), - {ok, maybe_set_api_repository(Repo4)}; - {undefined, undefined} -> + try rebar_hex_repos:get_repo_config(BinName, State) of + {ok, Repo} -> + {ok, set_http_adapter(Repo)} + catch + throw:{error, {rebar_hex_repos, {repo_not_found, _}}} -> {error, {not_valid_repo, RepoName}} end. - --define( ENV_VARS - , [ {"HEX_API_KEY", {api_key, {string, undefined}}} - , {"HEX_API_URL", {api_url, {string, undefined}}} - , {"HEX_UNSAFE_REGISTRY", {repo_verify, {boolean, false}}} - , {"HEX_NO_VERIFY_REPO_ORIGIN", {repo_verify_origin, {boolean, true}}} - ] - ). - -merge_with_env(Repo) -> - lists:foldl(fun({EnvName, {Key, _} = Default}, Acc) -> - Val = maybe_env_val(EnvName, Default), - maybe_put_key(Key, Val, Acc) - end, Repo, ?ENV_VARS). - -maybe_put_key(_Key, undefined, Repo) -> - Repo; -maybe_put_key(Key, Val, Repo) -> - case maps:get(Key, Repo, undefined) of - Val -> - Repo; - _ -> - Repo#{Key => Val} - end. - -maybe_env_val(K, {_, {Type, Default}}) -> - case {os:getenv(K), {Type, Default}} of - {false, {_, Default}} -> - Default; - {"", {_, Default}} -> - Default; - {Val, {boolean, _}} -> - to_bool(string:to_lower(Val)); - {Val, {string, _}} -> - rebar_utils:to_binary(Val) - end. - set_http_adapter(Repo) -> Repo#{http_adapter => {rebar3_hex_httpc_adapter, #{profile => rebar}}}. -to_bool("0") -> false; -to_bool("false") -> false; -to_bool(_) -> true. - parent_repos(State) -> Fun = fun(#{name := Name} = Repo, Acc) -> [Parent|_] = rebar3_hex_io:str_split(Name, <<":">>), @@ -194,7 +109,7 @@ parent_repos(State) -> true -> Acc; false -> - maps:put(name, Repo, Acc) + maps:put(name, set_http_adapter(Repo), Acc) end end, Map = lists:foldl(Fun, #{}, all_repos(State)), @@ -202,53 +117,3 @@ parent_repos(State) -> default_repo(State) -> rebar_hex_repos:get_repo_config(?DEFAULT_HEX_REPO, all_repos(State)). - -get_repo(BinaryName, Repos) -> - try rebar_hex_repos:get_repo_config(BinaryName, Repos) of - Name -> - Name - catch - {error,{rebar_hex_repos,{repo_not_found,BinaryName}}} -> undefined - end. - --spec get_hex_config(module(), map(), read | write) -> map(). -get_hex_config(Module, Repo, Mode) -> - case hex_config(Repo, Mode) of - {ok, HexConfig} -> - HexConfig; - {error, Reason} -> - erlang:error({error, {Module, {get_hex_config, Reason}}}) - end. - -hex_config(Repo, read) -> - hex_config_read(Repo); -hex_config(Repo, write) -> - hex_config_write(Repo). - -hex_config_write(#{api_key := Key} = HexConfig) when is_binary(Key) -> - {ok, set_http_adapter(HexConfig)}; -hex_config_write(#{write_key := undefined}) -> - {error, no_write_key}; -hex_config_write(#{write_key := WriteKey, username := Username} = HexConfig) -> - DecryptedWriteKey = rebar3_hex_user:decrypt_write_key(Username, WriteKey), - {ok, set_http_adapter(HexConfig#{api_key => DecryptedWriteKey})}; -hex_config_write(_) -> - {error, no_write_key}. - -hex_config_read(#{read_key := ReadKey} = HexConfig) -> - {ok, set_http_adapter(HexConfig#{api_key => ReadKey})}; -hex_config_read(_Config) -> - {error, no_read_key}. - -maybe_set_api_organization(#{name := Name} = Repo) -> - case binary:split(Name, <<":">>) of - [_] -> - Repo#{api_organization => undefined}; - [_,Org] -> - Repo#{api_organization => Org} - end. - -maybe_set_api_repository(#{api_repository := _} = Repo) -> - Repo; -maybe_set_api_repository(#{} = Repo) -> - Repo#{api_repository => undefined}. diff --git a/src/rebar3_hex_error.erl b/src/rebar3_hex_error.erl index 81ac5668..0704aab1 100644 --- a/src/rebar3_hex_error.erl +++ b/src/rebar3_hex_error.erl @@ -10,13 +10,11 @@ format_error({not_valid_repo, RepoName}) -> io_lib:format("Could not find ~ts in repo configuration. Be sure to authenticate first with rebar3 hex user auth.", [RepoName]); -format_error({get_hex_config, no_read_key}) -> - "No read key found for user. Be sure to authenticate first with:" - " rebar3 hex user auth"; +format_error({auth_error, no_credentials}) -> + "No Hex credentials found. Authenticate with: rebar3 hex user auth"; -format_error({get_hex_config, no_write_key}) -> - "No write key found for user. Be sure to authenticate first with:" - " rebar3 hex user auth"; +format_error({auth_error, Reason}) -> + io_lib:format("Authentication error: ~p", [Reason]); format_error({Cmd, unsupported_params}) -> io_lib:format("Either some or all of the parameters supplied for the ~ts command are ", [Cmd]) diff --git a/src/rebar3_hex_httpc_adapter.erl b/src/rebar3_hex_httpc_adapter.erl index 49ebe67d..8f2b4593 100644 --- a/src/rebar3_hex_httpc_adapter.erl +++ b/src/rebar3_hex_httpc_adapter.erl @@ -3,7 +3,7 @@ -module(rebar3_hex_httpc_adapter). -behaviour(hex_http). --export([request/5]). +-export([request/5, request_to_file/6]). %%==================================================================== %% API functions @@ -20,10 +20,54 @@ request(Method, URI, ReqHeaders, Body, AdapterConfig) -> {error, Reason} -> {error, Reason} end. +request_to_file(Method, URI, ReqHeaders, Body, Filename, AdapterConfig) -> + Profile = maps:get(profile, AdapterConfig, default), + Request = build_request(URI, ReqHeaders, Body), + SSLOpts = [{ssl, rebar_utils:ssl_opts(URI)}], + case httpc:request(Method, Request, SSLOpts, [{sync, false}, {stream, self}], Profile) of + {ok, RequestId} -> + stream_to_file(RequestId, Filename); + {error, Reason} -> + {error, Reason} + end. + %%==================================================================== %% Internal functions %%==================================================================== +%% @private +%% httpc streams 200/206 responses as messages and returns non-2xx as +%% a normal response tuple. stream_start includes the response headers. +stream_to_file(RequestId, Filename) -> + receive + {http, {RequestId, stream_start, Headers}} -> + {ok, File} = file:open(Filename, [write, binary]), + case stream_body(RequestId, File) of + ok -> + ok = file:close(File), + {ok, {200, load_headers(Headers)}}; + {error, Reason} -> + ok = file:close(File), + {error, Reason} + end; + {http, {RequestId, {{_, StatusCode, _}, RespHeaders, _RespBody}}} -> + {ok, {StatusCode, load_headers(RespHeaders)}}; + {http, {RequestId, {error, Reason}}} -> + {error, Reason} + end. + +%% @private +stream_body(RequestId, File) -> + receive + {http, {RequestId, stream, BinBodyPart}} -> + ok = file:write(File, BinBodyPart), + stream_body(RequestId, File); + {http, {RequestId, stream_end, _Headers}} -> + ok; + {http, {RequestId, {error, Reason}}} -> + {error, Reason} + end. + build_request(URI, ReqHeaders, Body) -> build_request2(binary_to_list(URI), dump_headers(ReqHeaders), Body). diff --git a/src/rebar3_hex_organization.erl b/src/rebar3_hex_organization.erl index 85c2ba86..3410f3fd 100644 --- a/src/rebar3_hex_organization.erl +++ b/src/rebar3_hex_organization.erl @@ -220,14 +220,24 @@ auth(State, RepoName) -> Key = case proplists:get_value(key, Opts, undefined) of undefined -> - Config = rebar3_hex_config:get_hex_config(?MODULE, ParentRepo, write), - Config1 = Config#{api_organization => OrgName}, KeyName = proplists:get_value(key_name, Opts, rebar3_hex_config:repos_key_name()), - generate_key(Config1, KeyName, default_perms(OrgName)); + case rebar_hex_auth:with_api(write, ParentRepo, State, [], fun(Config) -> + Config1 = Config#{api_organization => OrgName}, + rebar3_hex_key:generate(Config1, KeyName, default_perms(OrgName)) + end) of + {ok, #{<<"secret">> := Secret}} -> + Secret; + {error, #{<<"errors">> := Errors}} -> + ?RAISE({generate_key, Errors}); + {error, #{<<"message">> := Message}} -> + ?RAISE({generate_key, Message}); + Error -> + ?RAISE({generate_key, Error}) + end; ProvidedKey -> TestPerms = #{domain => <<"repository">>, resource => OrgName}, - Config = ParentRepo#{api_key => to_binary(ProvidedKey), - api_repository => OrgName, + Config = ParentRepo#{api_key => to_binary(ProvidedKey), + api_repository => OrgName, api_organization => OrgName }, case rebar3_hex_client:test_key(Config, TestPerms) of @@ -237,7 +247,7 @@ auth(State, RepoName) -> ?RAISE({auth, Error}) end end, - rebar3_hex_config:update_auth_config(#{RepoName => #{name => RepoName, repo_key => Key}}, State), + rebar_hex_repos:update_repo_auth_config(#{name => RepoName, repo_key => Key}, RepoName, State), rebar3_hex_io:say("Successfully authenticated to ~ts", [RepoName]), {ok, State}. @@ -249,21 +259,31 @@ deauth(State, RepoName) -> -spec generate(rebar_state:t(), binary()) -> {ok, rebar_state:t()}. generate(State, RepoName) -> - {Repo, OrgName} = get_parent_repo_and_org_name(State, RepoName), + {ParentRepo, OrgName} = get_parent_repo_and_org_name(State, RepoName), {Opts, _} = rebar_state:command_parsed_args(State), KeyName = proplists:get_value(key_name, Opts, rebar3_hex_config:repos_key_name()), - Config = rebar3_hex_config:get_hex_config(?MODULE, Repo, write), PermOpts = proplists:get_all_values(permission, Opts), Perms = rebar3_hex_key:convert_permissions(PermOpts, default_perms(OrgName)), - Key = generate_key(Config#{api_organization => OrgName}, KeyName, Perms), - rebar3_hex_io:say("~ts", [Key]), - {ok, State}. + case rebar_hex_auth:with_api(write, ParentRepo, State, [], fun(Config) -> + rebar3_hex_key:generate(Config#{api_organization => OrgName}, KeyName, Perms) + end) of + {ok, #{<<"secret">> := Key}} -> + rebar3_hex_io:say("~ts", [Key]), + {ok, State}; + {error, #{<<"errors">> := Errors}} -> + ?RAISE({key_generate, Errors}); + {error, #{<<"message">> := Message}} -> + ?RAISE({key_generate, Message}); + Error -> + ?RAISE({key_generate, Error}) + end. -spec list_org_keys(rebar_state:t(), binary()) -> {ok, rebar_state:t()}. list_org_keys(State, RepoName) -> - {Repo, OrgName} = get_parent_repo_and_org_name(State, RepoName), - Config = rebar3_hex_config:get_hex_config(?MODULE, Repo, read), - case rebar3_hex_key:list(Config#{api_organization => OrgName}) of + {ParentRepo, OrgName} = get_parent_repo_and_org_name(State, RepoName), + case rebar_hex_auth:with_api(read, ParentRepo, State, [{optional, false}], fun(Config) -> + rebar3_hex_key:list(Config#{api_organization => OrgName}) + end) of ok -> {ok, State}; {error, #{<<"errors">> := Errors}} -> @@ -276,7 +296,7 @@ list_org_keys(State, RepoName) -> -spec revoke(rebar_state:t(), binary()) -> {ok, rebar_state:t()}. revoke(State, RepoName) -> - {Repo, OrgName} = get_parent_repo_and_org_name(State, RepoName), + {ParentRepo, OrgName} = get_parent_repo_and_org_name(State, RepoName), {Opts, _} = rebar_state:command_parsed_args(State), KeyName = case proplists:get_value(key_name, Opts, undefined) of undefined -> @@ -284,8 +304,9 @@ revoke(State, RepoName) -> K -> K end, - Config = rebar3_hex_config:get_hex_config(?MODULE, Repo, write), - case rebar3_hex_key:revoke(Config#{api_organization => OrgName}, KeyName) of + case rebar_hex_auth:with_api(write, ParentRepo, State, [], fun(Config) -> + rebar3_hex_key:revoke(Config#{api_organization => OrgName}, KeyName) + end) of ok -> rebar3_hex_io:say("Key successfully revoked", []), {ok, State}; @@ -299,9 +320,10 @@ revoke(State, RepoName) -> -spec revoke_all(rebar_state:t(), binary()) -> {ok, rebar_state:t()}. revoke_all(State, RepoName) -> - {Repo, OrgName} = get_parent_repo_and_org_name(State, RepoName), - Config = rebar3_hex_config:get_hex_config(?MODULE, Repo, write), - case rebar3_hex_key:revoke_all(Config#{api_organization => OrgName}) of + {ParentRepo, OrgName} = get_parent_repo_and_org_name(State, RepoName), + case rebar_hex_auth:with_api(write, ParentRepo, State, [], fun(Config) -> + rebar3_hex_key:revoke_all(Config#{api_organization => OrgName}) + end) of ok -> rebar3_hex_io:say("All keys successfully revoked", []), {ok, State}; @@ -354,19 +376,6 @@ list_orgs(State) -> default_perms(OrgName) -> [#{<<"domain">> => <<"repository">>, <<"resource">> => OrgName}]. --spec generate_key(map(), binary() | undefined, [map()]) -> binary(). -generate_key(HexConfig, KeyName, Perms) -> - case rebar3_hex_key:generate(HexConfig, KeyName, Perms) of - {ok, #{<<"secret">> := Secret}} -> - Secret; - {error, #{<<"errors">> := Errors}} -> - ?RAISE({generate_key, Errors}); - {error, #{<<"message">> := Message}} -> - ?RAISE({generate_key, Message}); - Error -> - ?RAISE({generate_key, Error}) - end. - -spec printable_public_key(binary()) -> nonempty_string(). printable_public_key(PubKey) -> [Pem] = public_key:pem_decode(PubKey), diff --git a/src/rebar3_hex_owner.erl b/src/rebar3_hex_owner.erl index f3e1f07b..d49df2d2 100644 --- a/src/rebar3_hex_owner.erl +++ b/src/rebar3_hex_owner.erl @@ -105,26 +105,22 @@ handle_command(State, Repo) -> {"add", Package, UsernameOrEmail, Level, Transfer} -> case valid_level(Level) of true -> - Config = rebar3_hex_config:get_hex_config(?MODULE, Repo, write), - add(Config, Package, UsernameOrEmail, Level, Transfer, State), + add(State, Repo, Package, UsernameOrEmail, Level, Transfer), ok = rebar3_hex_io:say("Added ~ts to ~ts", [UsernameOrEmail, Package]), State; false -> ?RAISE({error, "level must be one of full or maintainer"}) end; {"remove", Package, UsernameOrEmail} -> - Config = rebar3_hex_config:get_hex_config(?MODULE, Repo, write), - remove(Config, Package, UsernameOrEmail, State), + remove(State, Repo, Package, UsernameOrEmail), ok = rebar3_hex_io:say("Removed ~ts to ~ts", [UsernameOrEmail, Package]), State; {"transfer", Package, UsernameOrEmail} -> - Config = rebar3_hex_config:get_hex_config(?MODULE, Repo, write), - add(Config, Package, UsernameOrEmail, <<"full">>, true, State), + add(State, Repo, Package, UsernameOrEmail, <<"full">>, true), ok = rebar3_hex_io:say("Transferred ~ts to ~ts", [Package, UsernameOrEmail]), State; {"list", Package} -> - Config = rebar3_hex_config:get_hex_config(?MODULE, Repo, read), - list(Config, Package, State); + list(State, Repo, Package); _Command -> ?RAISE(bad_command) end. @@ -202,11 +198,13 @@ valid_level(<<"full">>) -> true; valid_level(<<"maintainer">>) -> true; valid_level(_) -> false. -add(HexConfig, Package, UsernameOrEmail, Level, Transfer, State) -> - case hex_api_package_owner:add(HexConfig, Package, UsernameOrEmail, Level, Transfer) of - {ok, {Code, _Headers, _Body}} when Code =:= 204 orelse Code =:= 201-> - State; - {ok, {422, _Headers, #{<<"errors">> := Errors, <<"message">> := Message}}} -> +add(State, Repo, Package, UsernameOrEmail, Level, Transfer) -> + case rebar_hex_auth:with_api(write, Repo, State, [], fun(Config) -> + hex_api_package_owner:add(Config, Package, UsernameOrEmail, Level, Transfer) + end) of + {ok, {Code, _Headers, _Body}} when Code =:= 204 orelse Code =:= 201 -> + ok; + {ok, {422, _Headers, #{<<"errors">> := Errors, <<"message">> := Message}}} -> ?RAISE({validation_errors, add, Package, UsernameOrEmail, Errors, Message}); {ok, {Status, _Headers, _Body}} -> ?RAISE({status, Status, Package, UsernameOrEmail}); @@ -214,18 +212,22 @@ add(HexConfig, Package, UsernameOrEmail, Level, Transfer, State) -> ?RAISE({error, Package, UsernameOrEmail, Reason}) end. -remove(HexConfig, Package, UsernameOrEmail, State) -> - case hex_api_package_owner:delete(HexConfig, Package, UsernameOrEmail) of +remove(State, Repo, Package, UsernameOrEmail) -> + case rebar_hex_auth:with_api(write, Repo, State, [], fun(Config) -> + hex_api_package_owner:delete(Config, Package, UsernameOrEmail) + end) of {ok, {204, _Headers, _Body}} -> - State; + ok; {ok, {Status, _Headers, _Body}} -> ?RAISE({status, Status, Package, UsernameOrEmail}); {error, Reason} -> ?RAISE({error, Package, UsernameOrEmail, Reason}) end. -list(HexConfig, Package, State) -> - case hex_api_package_owner:list(HexConfig, Package) of +list(State, Repo, Package) -> + case rebar_hex_auth:with_api(read, Repo, State, [{optional, false}], fun(Config) -> + hex_api_package_owner:list(Config, Package) + end) of {ok, {200, _Headers, List}} -> Owners = [owner(Owner) || Owner <- List], OwnersString = rebar_string:join(Owners, "\n"), diff --git a/src/rebar3_hex_publish.erl b/src/rebar3_hex_publish.erl index 74ecd4e4..9dc8c0d2 100644 --- a/src/rebar3_hex_publish.erl +++ b/src/rebar3_hex_publish.erl @@ -299,9 +299,8 @@ handle_task(#{args := #{task := package, revert := Vsn}, apps := [App]} = Task) handle_task(#{args := #{task := package}, apps := [App]} = Task) -> maybe_warn_about_single_app_args(Task), #{args := Args, repo := Repo, state := State} = Task, - HexConfig = rebar3_hex_config:get_hex_config(?MODULE, Repo, write), rebar_api:info("package argument given, will not publish docs", []), - publish_package(State, HexConfig, App, Args); + publish_package(State, Repo, App, Args); handle_task(#{args := #{task := package, app := AppName}, apps := Apps} = Task) -> #{args := Args, repo := Repo, state := State} = Task, @@ -309,9 +308,8 @@ handle_task(#{args := #{task := package, app := AppName}, apps := Apps} = Task) {error, app_not_found} -> ?RAISE({app_not_found, AppName}); {ok, App} -> - HexConfig = rebar3_hex_config:get_hex_config(?MODULE, Repo, write), rebar_api:info("package argument given, will not publish docs", []), - publish_package(State, HexConfig, App, Args) + publish_package(State, Repo, App, Args) end, {ok, State}; @@ -388,12 +386,11 @@ handle_task(_) -> publish(State, Repo, App, Args) -> maybe_warn_about_doc_config(State, Repo), - HexConfig = rebar3_hex_config:get_hex_config(?MODULE, Repo, write), - case publish_package(State, HexConfig, App, Args) of + case publish_package(State, Repo, App, Args) of abort -> {ok, State}; _ -> - publish_docs(State, HexConfig, App, Args) + publish_docs(State, Repo, App, Args) end. @@ -417,14 +414,16 @@ publish_package(State, Repo, App, Args) -> rebar_api:info("--dry-run enabled : will not publish package.", []), {ok, State}; _ -> - case rebar3_hex_client:publish(Repo, Tarball, HexOpts) of + case rebar_hex_auth:with_api(write, Repo, State, [], fun(Config) -> + rebar3_hex_client:publish(Config, Tarball, HexOpts) + end) of {ok, _Res} -> #{name := Name, version := Version} = Package, rebar_api:info("Published ~ts ~ts", [Name, Version]), {ok, State}; - Error -> - #{name := Name, version := Version} = Package, - ?RAISE({publish_package, Name, Version, Error}) + Error -> + #{name := Name, version := Version} = Package, + ?RAISE({publish_package, Name, Version, Error}) end end; abort -> @@ -466,11 +465,11 @@ format_links(Links) -> string:join(LinksList, "\n "). %% if publishing to the public repo or to a private organization link to the code of conduct -maybe_say_coc(#{parent := <<"hexpm">>}) -> - rebar3_hex_io:say("Before publishing, please read Hex CoC: https://hex.pm/policies/codeofconduct", []); -maybe_say_coc(#{name := <<"hexpm">>}) -> +maybe_say_coc(#{repo_name := <<"hexpm">>, repo_organization := undefined}) -> rebar3_hex_io:say("Be aware, you are publishing to the public Hexpm repository.", []), rebar3_hex_io:say("Before publishing, please read Hex CoC: https://hex.pm/policies/codeofconduct", []); +maybe_say_coc(#{repo_name := <<"hexpm">>}) -> + rebar3_hex_io:say("Before publishing, please read Hex CoC: https://hex.pm/policies/codeofconduct", []); maybe_say_coc(_) -> ok. @@ -494,7 +493,7 @@ maybe_prompt(_Args, Message) -> abort end. -maybe_warn_about_doc_config(State, Repo) -> +maybe_warn_about_doc_config(State, Repo) -> case rebar3_hex_build:doc_opts(State, Repo) of undefined -> Warning = "No doc provider configuration was found, therefore docs can not be published.~n~n" @@ -537,15 +536,16 @@ hex_opts(Opts) -> %%% =================================================================== publish_docs(State, Repo, App, Args) -> - case create_docs(State, Repo, App, Args) of - #{tarball := Tar, name := Name, version := Vsn} -> + case create_docs(State, Repo, App, Args) of + #{tarball := Tar, name := Name, version := Vsn} -> case Args of #{dry_run := true} -> rebar_api:info("--dry-run enabled : will not publish docs.", []), {ok, State}; _ -> - Config = rebar3_hex_config:get_hex_config(?MODULE, Repo, write), - case rebar3_hex_client:publish_docs(Config, Name, Vsn, Tar) of + case rebar_hex_auth:with_api(write, Repo, State, [], fun(Config) -> + rebar3_hex_client:publish_docs(Config, Name, Vsn, Tar) + end) of {ok, _} -> rebar_api:info("Published docs for ~ts ~ts", [Name, Vsn]), {ok, State}; @@ -553,7 +553,7 @@ publish_docs(State, Repo, App, Args) -> ?RAISE({publish_docs, Name, Vsn, Reason}) end end; - ok -> + ok -> {ok, State} end. @@ -573,10 +573,11 @@ create_docs(State, Repo, App, Args) -> revert_package(State, Repo, AppName, Vsn) -> BinAppName = rebar_utils:to_binary(AppName), - BinVsn = rebar_utils:to_binary(Vsn), + BinVsn = rebar_utils:to_binary(Vsn), assert_valid_version_arg(BinVsn), - HexConfig = rebar3_hex_config:get_hex_config(?MODULE, Repo, write), - case rebar3_hex_client:delete_release(HexConfig, BinAppName, BinVsn) of + case rebar_hex_auth:with_api(write, Repo, State, [], fun(Config) -> + rebar3_hex_client:delete_release(Config, BinAppName, BinVsn) + end) of {ok, _} -> rebar_api:info("Successfully deleted package ~ts ~ts", [AppName, Vsn]), Prompt = io_lib:format("Also delete tag v~ts?", [Vsn]), @@ -592,10 +593,11 @@ revert_package(State, Repo, AppName, Vsn) -> revert_docs(State, Repo, AppName, Vsn) -> BinAppName = rebar_utils:to_binary(AppName), - BinVsn = rebar_utils:to_binary(Vsn), + BinVsn = rebar_utils:to_binary(Vsn), assert_valid_version_arg(BinVsn), - Config = rebar3_hex_config:get_hex_config(?MODULE, Repo, write), - case rebar3_hex_client:delete_docs(Config, BinAppName, BinVsn) of + case rebar_hex_auth:with_api(write, Repo, State, [], fun(Config) -> + rebar3_hex_client:delete_docs(Config, BinAppName, BinVsn) + end) of {ok, _} -> rebar_api:info("Successfully deleted docs for ~ts ~ts", [AppName, Vsn]), {ok, State}; diff --git a/src/rebar3_hex_repo.erl b/src/rebar3_hex_repo.erl new file mode 100644 index 00000000..3bfbe9ef --- /dev/null +++ b/src/rebar3_hex_repo.erl @@ -0,0 +1,591 @@ +%% @doc `rebar3 hex repo' - Manage Hex repository configuration. +%% +%% Subcommands: +%% - add: Add a new repository +%% - set: Update an existing repository +%% - remove: Remove a repository +%% - show: Display repository configuration +%% - list: List all configured repositories +%% +%%

Add a repository

+%% +%% Adds a new repository configuration. +%% +%% ``` +%% $ rebar3 hex repo add NAME URL +%% ''' +%% +%%

Update a repository

+%% +%% Updates an existing repository configuration. +%% +%% ``` +%% $ rebar3 hex repo set NAME [OPTIONS] +%% ''' +%% +%%

Remove a repository

+%% +%% Removes a repository configuration. +%% +%% ``` +%% $ rebar3 hex repo remove NAME +%% ''' +%% +%%

Show repository configuration

+%% +%% Displays the configuration for a repository. +%% +%% ``` +%% $ rebar3 hex repo show NAME +%% ''' +%% +%%

List repositories

+%% +%% Lists all configured repositories. +%% +%% ``` +%% $ rebar3 hex repo list +%% ''' +%% +%%

Command line options

+%% +%%
    +%%
  • `--public-key PATH' - Path to public key PEM file
  • +%%
  • `--fetch-public-key HASH' - Fetch public key from repository and verify SHA256 fingerprint
  • +%%
  • `--auth-key KEY' - API key for repository access
  • +%%
  • `--oauth-exchange' / `--no-oauth-exchange' - Enable/disable OAuth token exchange
  • +%%
  • `--oauth-exchange-url URL' - Custom OAuth exchange endpoint
  • +%%
  • `--url URL' - Repository URL (for set command)
  • +%%
  • `--api-url URL' - API URL (for set command)
  • +%%
+%% +%%

Examples

+%% +%% ``` +%% $ rebar3 hex repo add myrepo https://myrepo.example.com +%% $ rebar3 hex repo add myrepo https://myrepo.example.com --auth-key SECRET +%% $ rebar3 hex repo set myrepo --auth-key NEWKEY +%% $ rebar3 hex repo show myrepo +%% $ rebar3 hex repo remove myrepo +%% $ rebar3 hex repo list +%% ''' + +-module(rebar3_hex_repo). + +-behaviour(provider). + +-export([init/1, + do/1, + format_error/1]). + +-include("rebar3_hex.hrl"). + +-define(PROVIDER, repo). +-define(DEPS, []). + +%% =================================================================== +%% 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 repo list"}, + {short_desc, "Hex repository configuration commands"}, + {desc, "Manage Hex repository configuration.\n" + "\n" + "Subcommands:\n" + " add NAME URL Add a new repository\n" + " set NAME Update an existing repository\n" + " remove NAME Remove a repository\n" + " show NAME Display repository configuration\n" + " list List all configured repositories\n" + "\n" + "Options for add/set:\n" + " --public-key PATH Path to public key PEM file\n" + " --fetch-public-key HASH Fetch and verify public key (SHA256:...)\n" + " --auth-key KEY API key for repository access\n" + " --oauth-exchange Enable OAuth token exchange (default for hexpm)\n" + " --no-oauth-exchange Disable OAuth token exchange\n" + " --oauth-exchange-url Custom OAuth exchange endpoint\n" + "\n" + "Examples:\n" + " rebar3 hex repo add myrepo https://myrepo.example.com\n" + " rebar3 hex repo add myrepo https://myrepo.example.com --auth-key SECRET\n" + " rebar3 hex repo set myrepo --auth-key NEWKEY\n" + " rebar3 hex repo show myrepo\n" + " rebar3 hex repo remove myrepo\n" + " rebar3 hex repo list\n"}, + {opts, [ + {public_key, undefined, "public-key", string, + "Path to public key PEM file"}, + {fetch_public_key, undefined, "fetch-public-key", string, + "Fetch public key from repository and verify SHA256 fingerprint"}, + {auth_key, undefined, "auth-key", string, + "API key for repository access"}, + {oauth_exchange, undefined, "oauth-exchange", boolean, + "Enable OAuth token exchange"}, + {oauth_exchange_url, undefined, "oauth-exchange-url", string, + "Custom OAuth exchange endpoint"}, + {url, undefined, "url", string, + "Repository URL (for set command)"}, + {api_url, undefined, "api-url", string, + "API URL (for set command)"} + ]}]), + State1 = rebar_state:add_provider(State, Provider), + {ok, State1}. + +%% @private +-spec do(rebar_state:t()) -> {ok, rebar_state:t()}. +do(State) -> + {Args, _} = rebar_state:command_parsed_args(State), + case rebar_state:command_args(State) of + ["add", Name, Url | _] -> + do_add(Name, Url, Args, State); + ["add", Name | _] -> + %% URL might be in --url option + case proplists:get_value(url, Args) of + undefined -> + ?RAISE({missing_url, Name}); + Url -> + do_add(Name, Url, Args, State) + end; + ["add" | _] -> + ?RAISE(missing_repo_name); + ["set", Name | _] -> + do_set(Name, Args, State); + ["set" | _] -> + ?RAISE(missing_repo_name); + ["remove", Name | _] -> + do_remove(Name, State); + ["remove" | _] -> + ?RAISE(missing_repo_name); + ["show", Name | _] -> + do_show(Name, State); + ["show" | _] -> + ?RAISE(missing_repo_name); + ["list" | _] -> + do_list(State); + [] -> + rebar3_hex_io:say("Usage: rebar3 hex repo "), + rebar3_hex_io:say(""), + rebar3_hex_io:say("Subcommands:"), + rebar3_hex_io:say(" add NAME URL Add a new repository"), + rebar3_hex_io:say(" set NAME Update an existing repository"), + rebar3_hex_io:say(" remove NAME Remove a repository"), + rebar3_hex_io:say(" show NAME Display repository configuration"), + rebar3_hex_io:say(" list List all configured repositories"), + {ok, State}; + [Unknown | _] -> + ?RAISE({unknown_subcommand, Unknown}) + end. + +%% @private +-spec format_error(any()) -> iolist(). +format_error({unknown_subcommand, Cmd}) -> + io_lib:format("Unknown subcommand: ~ts", [Cmd]); +format_error(missing_repo_name) -> + "Missing repository name"; +format_error({missing_url, Name}) -> + io_lib:format("Missing URL for repository: ~ts", [Name]); +format_error({repo_exists, Name}) -> + io_lib:format("Repository already exists: ~ts. Use 'hex repo set' to update.", [Name]); +format_error({repo_not_found, Name}) -> + io_lib:format("Repository not found: ~ts", [Name]); +format_error({invalid_public_key, Path, Reason}) -> + io_lib:format("Failed to read public key from ~ts: ~p", [Path, Reason]); +format_error({fetch_public_key_failed, Status}) when is_integer(Status) -> + io_lib:format("Failed to fetch public key: HTTP ~p", [Status]); +format_error({fetch_public_key_failed, Reason}) -> + io_lib:format("Failed to fetch public key: ~p", [Reason]); +format_error({public_key_mismatch, Expected, Actual}) -> + io_lib:format("Public key fingerprint mismatch!~n" + " Expected: ~ts~n" + " Got: ~ts", [Expected, Actual]); +format_error({invalid_fingerprint_format, Fingerprint}) -> + io_lib:format("Invalid fingerprint format: ~ts~n" + "Expected format: SHA256:", [Fingerprint]); +format_error(fetch_public_key_requires_url) -> + "Cannot fetch public key: repository URL not set. Use --url or add URL first."; +format_error(cannot_remove_hexpm) -> + "Cannot remove the default hexpm repository"; +format_error(Reason) -> + rebar3_hex_error:format_error(Reason). + +%% =================================================================== +%% Add subcommand +%% =================================================================== + +-spec do_add(string(), string(), list(), rebar_state:t()) -> {ok, rebar_state:t()}. +do_add(Name, Url, Args, State) -> + RepoName = list_to_binary(Name), + AuthConfig = rebar3_hex_config:auth_config(State), + + %% Check if repo already exists + case maps:is_key(RepoName, AuthConfig) of + true -> + ?RAISE({repo_exists, Name}); + false -> + ok + end, + + %% Build repo config + RepoConfig0 = #{ + repo_url => list_to_binary(Url) + }, + + %% Add optional fields + RepoConfig1 = maybe_add_api_url(RepoConfig0, Url, Args), + RepoConfig2 = maybe_add_public_key(RepoConfig1, Args), + RepoConfig3 = maybe_fetch_public_key(RepoConfig2, Url, Args), + RepoConfig4 = maybe_add_auth_key(RepoConfig3, Args), + RepoConfig5 = maybe_add_oauth_exchange(RepoConfig4, Args), + RepoConfig6 = maybe_add_oauth_exchange_url(RepoConfig5, Args), + + %% Save to hex.config + rebar3_hex_config:update_repo_auth_config(RepoConfig6, RepoName, State), + + rebar3_hex_io:say("Added repository ~ts", [Name]), + {ok, State}. + +%% =================================================================== +%% Set subcommand +%% =================================================================== + +-spec do_set(string(), list(), rebar_state:t()) -> {ok, rebar_state:t()}. +do_set(Name, Args, State) -> + RepoName = list_to_binary(Name), + AuthConfig = rebar3_hex_config:auth_config(State), + + %% Get existing config or empty map + ExistingConfig = maps:get(RepoName, AuthConfig, #{}), + + %% Update fields + RepoConfig0 = maybe_update_url(ExistingConfig, Args), + RepoConfig1 = maybe_update_api_url(RepoConfig0, Args), + RepoConfig2 = maybe_add_public_key(RepoConfig1, Args), + RepoConfig3 = maybe_fetch_public_key_for_set(RepoConfig2, Args), + RepoConfig4 = maybe_add_auth_key(RepoConfig3, Args), + RepoConfig5 = maybe_add_oauth_exchange(RepoConfig4, Args), + RepoConfig6 = maybe_add_oauth_exchange_url(RepoConfig5, Args), + + %% Save to hex.config + rebar3_hex_config:update_repo_auth_config(RepoConfig6, RepoName, State), + + rebar3_hex_io:say("Updated repository ~ts", [Name]), + {ok, State}. + +%% =================================================================== +%% Remove subcommand +%% =================================================================== + +-spec do_remove(string(), rebar_state:t()) -> {ok, rebar_state:t()}. +do_remove(Name, State) -> + RepoName = list_to_binary(Name), + + %% Don't allow removing hexpm + case RepoName of + <<"hexpm">> -> + ?RAISE(cannot_remove_hexpm); + _ -> + ok + end, + + AuthConfig = rebar3_hex_config:auth_config(State), + + case maps:is_key(RepoName, AuthConfig) of + true -> + rebar3_hex_config:remove_from_auth_config(RepoName, State), + rebar3_hex_io:say("Removed repository ~ts", [Name]), + {ok, State}; + false -> + ?RAISE({repo_not_found, Name}) + end. + +%% =================================================================== +%% Show subcommand +%% =================================================================== + +-spec do_show(string(), rebar_state:t()) -> {ok, rebar_state:t()}. +do_show(Name, State) -> + RepoName = list_to_binary(Name), + AuthConfig = rebar3_hex_config:auth_config(State), + + case maps:find(RepoName, AuthConfig) of + {ok, RepoConfig} -> + rebar3_hex_io:say("Repository: ~ts", [Name]), + rebar3_hex_io:say(""), + print_repo_config(RepoConfig), + {ok, State}; + error -> + %% Check if it's a built-in repo from rebar.config + try + {ok, Config} = rebar_hex_repos:get_repo_config(RepoName, State), + rebar3_hex_io:say("Repository: ~ts (built-in)", [Name]), + rebar3_hex_io:say(""), + print_repo_config(Config), + {ok, State} + catch + _:_ -> + ?RAISE({repo_not_found, Name}) + end + end. + +%% @private +-spec print_repo_config(map()) -> ok. +print_repo_config(Config) -> + %% Print URL + case maps:find(repo_url, Config) of + {ok, RepoUrl} -> rebar3_hex_io:say(" URL: ~ts", [RepoUrl]); + error -> ok + end, + + %% Print API URL + case maps:find(api_url, Config) of + {ok, ApiUrl} -> rebar3_hex_io:say(" API URL: ~ts", [ApiUrl]); + error -> ok + end, + + %% Print public key (truncated) + case maps:find(repo_public_key, Config) of + {ok, PublicKey} when is_binary(PublicKey) -> + KeyPreview = truncate_key(PublicKey), + rebar3_hex_io:say(" Public Key: ~ts", [KeyPreview]); + _ -> + ok + end, + + %% Print auth key (masked) + case maps:find(auth_key, Config) of + {ok, AuthKey} when is_binary(AuthKey) -> + Masked = mask_key(AuthKey), + rebar3_hex_io:say(" Auth Key: ~ts", [Masked]); + _ -> + ok + end, + + %% Print OAuth exchange setting + case maps:find(oauth_exchange, Config) of + {ok, OAuthExchange} -> + rebar3_hex_io:say(" OAuth Exchange: ~p", [OAuthExchange]); + error -> + ok + end, + + %% Print OAuth exchange URL + case maps:find(oauth_exchange_url, Config) of + {ok, OAuthUrl} -> + rebar3_hex_io:say(" OAuth Exchange URL: ~ts", [OAuthUrl]); + error -> + ok + end, + + ok. + +%% @private +-spec truncate_key(binary()) -> binary(). +truncate_key(Key) -> + case byte_size(Key) > 50 of + true -> + <> = Key, + <>; + false -> + Key + end. + +%% @private +-spec mask_key(binary()) -> binary(). +mask_key(Key) -> + case byte_size(Key) of + Len when Len > 8 -> + <> = Key, + <>; + _ -> + <<"****">> + end. + +%% =================================================================== +%% List subcommand +%% =================================================================== + +-spec do_list(rebar_state:t()) -> {ok, rebar_state:t()}. +do_list(State) -> + AuthConfig = rebar3_hex_config:auth_config(State), + + %% Get repos from auth config (excluding $oauth) + AuthRepos = [Name || Name <- maps:keys(AuthConfig), Name =/= <<"$oauth">>], + + %% Get repos from rebar.config + Resources = rebar_state:resources(State), + ConfigRepos = case rebar_resource_v2:find_resource_state(pkg, Resources) of + #{repos := Repos} -> + [maps:get(name, R) || R <- Repos]; + _ -> + [] + end, + + %% Combine and deduplicate + AllRepos = lists:usort(AuthRepos ++ ConfigRepos), + + case AllRepos of + [] -> + rebar3_hex_io:say("No repositories configured."); + _ -> + rebar3_hex_io:say("Repositories:"), + lists:foreach(fun(RepoName) -> + Source = case {lists:member(RepoName, AuthRepos), lists:member(RepoName, ConfigRepos)} of + {true, true} -> " (hex.config + rebar.config)"; + {true, false} -> " (hex.config)"; + {false, true} -> " (rebar.config)"; + {false, false} -> "" + end, + rebar3_hex_io:say(" ~ts~ts", [RepoName, Source]) + end, AllRepos) + end, + {ok, State}. + +%% =================================================================== +%% Internal functions +%% =================================================================== + +%% @private +-spec maybe_add_api_url(map(), string(), list()) -> map(). +maybe_add_api_url(Config, Url, Args) -> + case proplists:get_value(api_url, Args) of + undefined -> + %% Derive API URL from repo URL + ApiUrl = derive_api_url(Url), + Config#{api_url => list_to_binary(ApiUrl)}; + ApiUrl -> + Config#{api_url => list_to_binary(ApiUrl)} + end. + +%% @private +-spec derive_api_url(string()) -> string(). +derive_api_url(Url) -> + %% Simple heuristic: append /api if not already present + case lists:suffix("/api", Url) of + true -> Url; + false -> + case lists:suffix("/", Url) of + true -> Url ++ "api"; + false -> Url ++ "/api" + end + end. + +%% @private +-spec maybe_update_url(map(), list()) -> map(). +maybe_update_url(Config, Args) -> + case proplists:get_value(url, Args) of + undefined -> Config; + Url -> Config#{repo_url => list_to_binary(Url)} + end. + +%% @private +-spec maybe_update_api_url(map(), list()) -> map(). +maybe_update_api_url(Config, Args) -> + case proplists:get_value(api_url, Args) of + undefined -> Config; + ApiUrl -> Config#{api_url => list_to_binary(ApiUrl)} + end. + +%% @private +-spec maybe_add_public_key(map(), list()) -> map(). +maybe_add_public_key(Config, Args) -> + case proplists:get_value(public_key, Args) of + undefined -> + Config; + Path -> + case file:read_file(Path) of + {ok, PemData} -> + Config#{repo_public_key => PemData}; + {error, Reason} -> + ?RAISE({invalid_public_key, Path, Reason}) + end + end. + +%% @private +-spec maybe_fetch_public_key(map(), string(), list()) -> map(). +maybe_fetch_public_key(Config, Url, Args) -> + case proplists:get_value(fetch_public_key, Args) of + undefined -> + Config; + ExpectedFingerprint -> + fetch_and_verify_public_key(Config, Url, ExpectedFingerprint) + end. + +%% @private +-spec maybe_fetch_public_key_for_set(map(), list()) -> map(). +maybe_fetch_public_key_for_set(Config, Args) -> + case proplists:get_value(fetch_public_key, Args) of + undefined -> + Config; + ExpectedFingerprint -> + case maps:find(repo_url, Config) of + {ok, Url} -> + fetch_and_verify_public_key(Config, binary_to_list(Url), + ExpectedFingerprint); + error -> + ?RAISE(fetch_public_key_requires_url) + end + end. + +%% @private +-spec fetch_and_verify_public_key(map(), string(), string()) -> map(). +fetch_and_verify_public_key(Config, Url, ExpectedFingerprint) -> + %% Build config for HTTP request by merging with defaults + RepoUrl = list_to_binary(Url), + FetchConfig = maps:merge(hex_core:default_config(), #{ + repo_url => RepoUrl, + http_adapter => {hex_http_httpc, #{profile => default}} + }), + + case hex_repo:get_public_key(FetchConfig) of + {ok, {200, _Headers, PublicKey}} -> + verify_and_store_key(Config, PublicKey, ExpectedFingerprint); + {ok, {Status, _Headers, _Body}} -> + ?RAISE({fetch_public_key_failed, Status}); + {error, Reason} -> + ?RAISE({fetch_public_key_failed, Reason}) + end. + +%% @private +-spec verify_and_store_key(map(), binary(), string()) -> map(). +verify_and_store_key(Config, PublicKey, ExpectedFingerprint) -> + case hex_repo:fingerprint_equal(PublicKey, ExpectedFingerprint) of + true -> + Config#{repo_public_key => PublicKey}; + false -> + ActualFingerprint = hex_repo:fingerprint(PublicKey), + ?RAISE({public_key_mismatch, ExpectedFingerprint, ActualFingerprint}) + end. + +%% @private +-spec maybe_add_auth_key(map(), list()) -> map(). +maybe_add_auth_key(Config, Args) -> + case proplists:get_value(auth_key, Args) of + undefined -> Config; + AuthKey -> Config#{auth_key => list_to_binary(AuthKey)} + end. + +%% @private +-spec maybe_add_oauth_exchange(map(), list()) -> map(). +maybe_add_oauth_exchange(Config, Args) -> + case proplists:get_value(oauth_exchange, Args) of + undefined -> Config; + Value -> Config#{oauth_exchange => Value} + end. + +%% @private +-spec maybe_add_oauth_exchange_url(map(), list()) -> map(). +maybe_add_oauth_exchange_url(Config, Args) -> + case proplists:get_value(oauth_exchange_url, Args) of + undefined -> Config; + Url -> Config#{oauth_exchange_url => list_to_binary(Url)} + end. diff --git a/src/rebar3_hex_retire.erl b/src/rebar3_hex_retire.erl index 07b2277f..50ef61ab 100644 --- a/src/rebar3_hex_retire.erl +++ b/src/rebar3_hex_retire.erl @@ -125,10 +125,11 @@ format_error(Reason) -> rebar3_hex_error:format_error(Reason). retire(State, PkgName, Version, Repo, RetireReason, RetireMessage) -> - HexConfig = rebar3_hex_config:get_hex_config(?MODULE, Repo, write), Msg = #{<<"reason">> => RetireReason, - <<"message">> => RetireMessage}, - case hex_api_release:retire(HexConfig, PkgName, Version, Msg) of + <<"message">> => RetireMessage}, + case rebar_hex_auth:with_api(write, Repo, State, [], fun(Config) -> + hex_api_release:retire(Config, PkgName, Version, Msg) + end) of {ok, {204, _Headers, _Body}} -> rebar_api:info("Successfully retired package ~ts ~ts", [PkgName, Version]), {ok, State}; diff --git a/src/rebar3_hex_search.erl b/src/rebar3_hex_search.erl index 769f262e..5cd28bfe 100644 --- a/src/rebar3_hex_search.erl +++ b/src/rebar3_hex_search.erl @@ -56,8 +56,9 @@ do(State) -> {ok, State}. search(State, Repo, Term) -> - HexConfig = rebar3_hex_config:get_hex_config(?MODULE, Repo, read), - case hex_api_package:search(HexConfig, rebar_utils:to_binary(Term), []) of + case rebar_hex_auth:with_api(read, Repo, State, [{optional, true}], fun(Config) -> + hex_api_package:search(Config, rebar_utils:to_binary(Term), []) + end) of {ok, {200, _Headers, []}} -> io:format("No Results~n"), {ok, State}; diff --git a/src/rebar3_hex_user.erl b/src/rebar3_hex_user.erl index a661b93e..cb82ccb2 100644 --- a/src/rebar3_hex_user.erl +++ b/src/rebar3_hex_user.erl @@ -15,12 +15,15 @@ %%

Authorize a new user

%% %% ``` -%% $ rebar3 hex user auth [--key-name KEY_NAME] +%% $ rebar3 hex user auth %% ''' %% +%% This opens a browser to complete authentication. Use `--no-browser' to +%% disable automatic browser opening. +%% %%

Deauthorize the user

%% -%% Deauthorizes the user from the local machine by removing the API key from the Hex config. +%% Deauthorizes the user from the local machine by removing the API tokens from the Hex config. %% %% ``` %% $ rebar3 hex user deauth @@ -67,12 +70,6 @@ %% rebar3 hex user reset_password account %% ''' %% -%%

Reset local password

-%% -%% ``` -%% rebar3 hex user reset_password local -%% ''' -%% %%

Command line options

%% %%
    @@ -100,6 +97,9 @@ %%
  • `repositories' - Access to repositories for all organizations you are member of.
  • %%
%% +%%
  • +%% `--browser / --no-browser' - Open browser automatically during authentication (default: true). +%%
  • %% -module(rebar3_hex_user). @@ -108,8 +108,6 @@ do/1, format_error/1]). --export([encrypt_write_key/3, - decrypt_write_key/2]). -include("rebar3_hex.hrl"). @@ -135,7 +133,9 @@ init(State) -> rebar3_hex:repo_opt(), {all, $a, "all", boolean, "all."}, {key_name, $k, "key-name", string, "key-name"}, - {permission, $p, "permission", list, "perms."} + {permission, $p, "permission", list, "perms."}, + {browser, $b, "browser", {boolean, true}, + "Open browser automatically (default: true)"} ] }]), State1 = rebar_state:add_provider(State, Provider), @@ -153,17 +153,13 @@ do(State) -> %% @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(passwords_do_not_match) -> + "Password confirmation failed. The passwords must match."; 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]); @@ -175,27 +171,27 @@ 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({auth_failed, Reason}) -> + io_lib:format("Authentication failed: ~p", [Reason]); +format_error({revoke_failed, Status, Body}) -> + io_lib:format("Failed to revoke token (~p): ~p", [Status, Body]); format_error(bad_command) -> "Invalid arguments, expected one of:\n\n" "rebar3 hex user register\n" "rebar3 hex user auth\n" + "rebar3 hex user auth --no-browser\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"; + "rebar3 hex user key generate\n" + "rebar3 hex user key revoke --key-name KEY_NAME\n" + "rebar3 hex user key revoke --all\n" + "rebar3 hex user key list\n" + "rebar3 hex user key fetch --key-name KEY_NAME\n" + "rebar3 hex user reset_password account\n"; format_error(Reason) -> rebar3_hex_error:format_error(Reason). @@ -210,60 +206,65 @@ handle_task(#{args := #{task := register}} = Task) -> create_user(Username, Email, Password, Repo, State); handle_task(#{args := #{task := auth}} = Task) -> - #{repo := #{repo_name := RepoName} = Repo, state := State} = Task, - Username = get_string_input("Username"), - Password = get_password(account), - - Auth = base64:encode_to_string(<>), - 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. - rebar3_hex_config:update_auth_config(#{RepoName => #{ - 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}; + #{state := State, raw_opts := Opts} = Task, + + OpenBrowser = proplists:get_value(browser, Opts, true), + {ok, Config} = rebar_hex_repos:get_repo_config(<<"hexpm">>, State), + ClientId = rebar_hex_auth:client_id(), + GlobalOAuthKey = rebar_hex_auth:global_oauth_key(), + + maybe_revoke_existing(Config, ClientId, GlobalOAuthKey, State), + + Scope = <<"api:write">>, + PromptUser = fun(VerificationUri, UserCode) -> + rebar3_hex_io:say(""), + rebar3_hex_io:say("Open this URL in your browser to authenticate:"), + rebar3_hex_io:say("~ts", [VerificationUri]), + rebar3_hex_io:say(""), + rebar3_hex_io:say("Enter code: ~ts", [UserCode]), + rebar3_hex_io:say(""), + rebar3_hex_io:say("Waiting for authentication..."), + ok + end, + + FlowOpts = [{open_browser, OpenBrowser}], + case hex_api_oauth:device_auth_flow(Config, ClientId, Scope, PromptUser, FlowOpts) of + {ok, #{access_token := AccessToken, expires_at := ExpiresAt} = Tokens} -> + RefreshToken = maps:get(refresh_token, Tokens, undefined), + rebar_hex_auth:persist_tokens(AccessToken, RefreshToken, ExpiresAt, State), + rebar3_hex_io:say(""), + rebar3_hex_io:say("Authentication successful!"), + %% Show authenticated user + BearerToken = <<"Bearer ", AccessToken/binary>>, + AuthConfig = Config#{api_key => BearerToken}, + case hex_api_user:me(AuthConfig) of + {ok, {200, _, UserInfo}} -> + Username = maps:get(<<"username">>, UserInfo, <<"unknown">>), + Email = maps:get(<<"email">>, UserInfo, <<"unknown">>), + rebar3_hex_io:say("Authenticated as: ~ts (~ts)", [Username, Email]); + _ -> + ok + end, + {ok, State}; + {error, Reason} -> + ?RAISE({auth_failed, Reason}) + end; 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]), + #{state := State} = Task, + + {ok, Config} = rebar_hex_repos:get_repo_config(<<"hexpm">>, State), + ClientId = rebar_hex_auth:client_id(), + GlobalOAuthKey = rebar_hex_auth:global_oauth_key(), + + case rebar_hex_repos:get_repo_auth_config(GlobalOAuthKey, State) of + #{access_token := AccessToken} when is_binary(AccessToken) -> + _ = hex_api_oauth:revoke_token(Config, ClientId, AccessToken), + rebar_hex_repos:remove_from_auth_config(GlobalOAuthKey, State), + rebar3_hex_io:say("Authentication removed."), {ok, State}; _ -> - rebar3_hex_io:say("Not authenticated as any user currently for this repository"), + rebar3_hex_io:say("Not authenticated."), {ok, State} end; @@ -280,31 +281,28 @@ handle_task(#{args := #{task := reset_password, account := true}} = Task) -> ?RAISE({reset_account_password, Error}) end; -%% TODO: Write a test -handle_task(#{args := #{task := reset_password, local := true}} = Task) -> +handle_task(#{args := #{task := whoami}} = 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), - + #{name := RepoName} = Repo, + case rebar_hex_auth:with_api(read, Repo, State, [{optional, false}, {auth_inline, false}], fun(Config) -> + rebar3_hex_client:me(Config) + end) of + {ok, #{<<"username">> := UserName} = Res} -> + Header = ["Repo", "User Name", "Full Name", "Email"], + FullName = maps:get(<<"full_name">>, Res, <<"private">>), + Email = maps:get(<<"email">>, Res, <<"private">>), + Body = [binary_to_list(RepoName), binary_to_list(UserName), binary_to_list(FullName), binary_to_list(Email)], + ok = rebar3_hex_results:print_table([Header] ++ [Body]), {ok, State}; - _ -> + {error, {auth_error, no_credentials}} -> rebar3_hex_io:say("Not authenticated as any user currently for this repository"), - {ok, State} + {ok, State}; + {error, #{<<"message">> := Message}} -> + ?RAISE({whoami, Message}); + Err -> + ?RAISE({whoami, Err}) end; -handle_task(#{args := #{task := whoami}} = Task) -> - #{repo := Repo, state := State} = Task, - whoami(Repo, State), - {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), @@ -320,8 +318,9 @@ handle_task(#{args := #{task := key, generate := true} = Args} = Task) -> 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 + case rebar_hex_auth:with_api(write, Repo, State, [], fun(Config) -> + rebar3_hex_key:revoke_all(Config) + end) of ok -> rebar3_hex_io:say("All keys successfully revoked", []), {ok, State}; @@ -331,8 +330,9 @@ handle_task(#{args := #{task := key, revoke := true, all := true}} = Task) -> 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 + case rebar_hex_auth:with_api(write, Repo, State, [], fun(Config) -> + rebar3_hex_key:revoke(Config, KeyName) + end) of ok -> rebar3_hex_io:say("Key successfully revoked", []), {ok, State}; @@ -341,9 +341,10 @@ handle_task(#{args := #{task := key, revoke := true, key_name := KeyName}} = Tas 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 -> + case rebar_hex_auth:with_api(read, Repo, State, [{optional, false}], fun(Config) -> + rebar3_hex_key:list(Config) + end) of + ok -> {ok, State}; Error -> ?RAISE({key_list, Error}) @@ -351,9 +352,10 @@ handle_task(#{repo := Repo, state := State, args := #{task := key, list := true} 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 -> + case rebar_hex_auth:with_api(read, Repo, State, [{optional, false}], fun(Config) -> + rebar3_hex_key:fetch(Config, KeyName) + end) of + ok -> {ok, State}; Error -> ?RAISE({key_list, Error}) @@ -362,26 +364,6 @@ handle_task(#{args := #{task := key, fetch := true, key_name := KeyName}} = Task handle_task(_) -> ?RAISE(bad_command). -whoami(#{name := Name} = Repo, State) -> - case maps:get(read_key, Repo, undefined) of - undefined -> - ?RAISE(not_authenticated); - ReadKey -> - case rebar3_hex_client:me(Repo#{api_key => ReadKey}) of - {ok, #{<<"username">> := UserName} = Res} -> - Header = ["Repo", "User Name", "Full Name", "Email"], - FullName = maps:get(<<"full_name">>, Res, <<"private">>), - Email = maps:get(<<"email">>, Res, <<"private">>), - Body = [binary_to_list(Name), binary_to_list(UserName), binary_to_list(FullName), binary_to_list(Email)], - ok = rebar3_hex_results:print_table([Header] ++ [Body]), - {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). @@ -398,44 +380,28 @@ do_get_string_input(Prompt, MaxRetries) -> 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) -> + get_password(<<"Account">>); +get_password(Type) when is_binary(Type) -> MaxRetries = 3, - do_get_password(Type, Fun, MaxRetries). + do_get_password(Type, MaxRetries). -do_get_password(_, _, 0) -> +do_get_password(_, 0) -> ?RAISE(passwords_do_not_match); -do_get_password(Type, Fun, MaxRetries) -> +do_get_password(Type, MaxRetries) -> Rest = <<" Password: ">>, Password = rebar3_hex_io:get_password(<>), - 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, Password, MaxRetries). -confirm_password(Type, Fun, ExpectedPw, MaxRetries) -> +confirm_password(Type, ExpectedPw, MaxRetries) -> Rest = <<" Password (confirm): ">>, case rebar3_hex_io:get_password(<>) of Pw when Pw =:= ExpectedPw -> Pw; _ -> rebar_api:warn("Passwords do not match, please try again", []), - do_get_password(Type, Fun, MaxRetries - 1) + do_get_password(Type, MaxRetries - 1) end. create_user(Username, Email, Password, Repo, State) -> @@ -443,8 +409,7 @@ create_user(Username, Email, Password, Repo, State) -> {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)]), + rebar3_hex_io:say("Then run `rebar3 hex user auth` to authenticate.", []), {ok, State}; {error, #{<<"errors">> := Errors}} -> ?RAISE({registration_failure, Errors}); @@ -452,48 +417,6 @@ create_user(Username, Email, Password, Repo, State) -> ?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. - generate_key(HexConfig, KeyName, Perms) -> case rebar3_hex_key:generate(HexConfig, KeyName, Perms) of {ok, #{<<"secret">> := Secret}} -> @@ -504,17 +427,14 @@ generate_key(HexConfig, KeyName, Perms) -> ?RAISE({generate_key, Error}) end. -hostname() -> - {ok, Name} = inet:gethostname(), - Name. - -api_key_name() -> - rebar_utils:to_binary(hostname()). - -api_key_name(Postfix) -> - rebar_utils:to_binary([hostname(), "-api-", Postfix]). - -repos_key_name() -> - rebar_utils:to_binary([hostname(), "-repositories"]). - +maybe_revoke_existing(Config, ClientId, GlobalOAuthKey, State) -> + case rebar_hex_repos:get_repo_auth_config(GlobalOAuthKey, State) of + #{access_token := AccessToken} when is_binary(AccessToken) -> + BearerToken = <<"Bearer ", AccessToken/binary>>, + AuthedConfig = Config#{api_key => BearerToken}, + _ = hex_api_oauth:revoke_token(AuthedConfig, ClientId, AccessToken), + ok; + _ -> + ok + end. diff --git a/test/rebar3_hex_config_SUITE.erl b/test/rebar3_hex_config_SUITE.erl index d6a3659d..fc71ba6c 100644 --- a/test/rebar3_hex_config_SUITE.erl +++ b/test/rebar3_hex_config_SUITE.erl @@ -5,25 +5,12 @@ -include_lib("eunit/include/eunit.hrl"). all() -> - [repo, api_key_name_test, encrypt_decrypt_write_key_test, org_key_name_test]. + [repo, api_key_name_test, org_key_name_test]. api_key_name_test(_Config) -> ?assertEqual(<<"foo-api">>, rebar3_hex_config:api_key_name(<<"foo">>)), ?assertEqual(<<"foo-api-org">>, rebar3_hex_config:api_key_name(<<"foo">>, <<"org">>)). -encrypt_decrypt_write_key_test(_Config) -> - WriteKey = <<"abc1234">>, - - Username = <<"user">>, - LocalPassword = <<"password">>, - - WriteKeyEncrypted = rebar3_hex_user:encrypt_write_key(Username, LocalPassword, WriteKey), - - ?assertMatch(error, - rebar3_hex_config:decrypt_write_key(Username, <<"wrong password">>, WriteKeyEncrypted)), - - ?assertEqual(WriteKey, rebar3_hex_config:decrypt_write_key(Username, LocalPassword, WriteKeyEncrypted)). - org_key_name_test(_Config) -> {ok, Name} = inet:gethostname(), BinName = list_to_binary(Name), diff --git a/test/rebar3_hex_integration_SUITE.erl b/test/rebar3_hex_integration_SUITE.erl index dcd29491..ebe754dd 100644 --- a/test/rebar3_hex_integration_SUITE.erl +++ b/test/rebar3_hex_integration_SUITE.erl @@ -11,10 +11,8 @@ all() -> [ sanity_check - , decrypt_write_key_test , bad_command_test , reset_password_test - , reset_local_password_test , reset_password_error_test , reset_password_api_error_test , register_user_test @@ -23,16 +21,13 @@ all() -> , register_error_test , register_existing_user_test , auth_test - , auth_bad_local_password_test - , auth_password_24_char_test - , auth_password_32_char_test - , auth_unhandled_test , auth_error_test , whoami_test , whoami_not_authed_test , whoami_error_test , whoami_unknown_test , deauth_test + , deauth_not_authenticated_test , org_auth_test , org_auth_key_test , org_deauth_test @@ -90,6 +85,17 @@ all() -> , owner_transfer_test , owner_list_test , owner_remove_test + , repo_no_args_test + , repo_unknown_subcommand_test + , repo_add_missing_name_test + , repo_add_missing_url_test + , repo_remove_hexpm_test + , repo_list_test + , repo_add_test + , repo_add_with_auth_key_test + , repo_set_test + , repo_show_test + , repo_remove_test ]. init_per_suite(Config) -> @@ -110,6 +116,8 @@ end_per_testcase(_Tc, Config) -> MockPid = ?config(hex_mock_server, Config), ok = hex_db:stop(StorePid), ok = elli:stop(MockPid), + %% Clear repo auth config state used by repo tests + erase(repo_auth_config), reset_mocks([rebar3_hex_config, rebar3_hex_io, hex_api_user]), Config. @@ -226,19 +234,19 @@ register_password_mismatch_test(Config) -> auth_test(Config) -> P = #{ - command => #{provider => rebar3_hex_user, args => ["auth"]}, + command => #{provider => rebar3_hex_user, args => ["auth", "--browser", "false"]}, app => #{name => "valid"}, - mocks => [first_auth] + mocks => [oauth_auth] }, - #{rebar_state := State, repo := Repo} = setup_state(P, Config), - create_user(?default_username, ?default_password, ?default_email, Repo), + #{rebar_state := State} = setup_state(P, Config), ?assertMatch({ok, State}, rebar3_hex_user:do(State)). org_auth_test(Config) -> P = #{ command => #{provider => rebar3_hex_organization, args => ["auth", "hexpm:foo"]}, app => #{name => "valid"}, - repo_config => #{repo => <<"hexpm:foo">>, name => <<"hexpm:foo">>}, + repo_config => #{repo => <<"hexpm:foo">>, name => <<"hexpm:foo">>, + repo_name => <<"hexpm">>, repo_organization => <<"foo">>}, mocks => [] }, #{rebar_state := State} = Setup = setup_state(P, Config), @@ -252,7 +260,8 @@ org_auth_key_test(Config) -> P = #{ command => #{provider => rebar3_hex_organization, args => ["auth", "hexpm:foo", "--key", "123"]}, app => #{name => "valid"}, - repo_config => #{repo => <<"hexpm:foo">>, name => <<"hexpm:foo">>}, + repo_config => #{repo => <<"hexpm:foo">>, name => <<"hexpm:foo">>, + repo_name => <<"hexpm">>, repo_organization => <<"foo">>}, mocks => [] }, #{rebar_state := State} = Setup = setup_state(P, Config), @@ -266,7 +275,8 @@ org_deauth_test(Config) -> P = #{ command => #{provider => rebar3_hex_organization, args => ["deauth", "hexpm:foo"]}, app => #{name => "valid"}, - repo_config => #{repo => <<"hexpm:foo">>, name => <<"hexpm:foo">>}, + repo_config => #{repo => <<"hexpm:foo">>, name => <<"hexpm:foo">>, + repo_name => <<"hexpm">>, repo_organization => <<"foo">>}, mocks => [] }, #{rebar_state := State} = Setup = setup_state(P, Config), @@ -280,7 +290,8 @@ org_key_generate_test(Config) -> P = #{ command => #{provider => rebar3_hex_organization, args => ["key", "hexpm:foo", "generate"]}, app => #{name => "valid"}, - repo_config => #{repo => <<"hexpm:foo">>, name => <<"hexpm:foo">>}, + repo_config => #{repo => <<"hexpm:foo">>, name => <<"hexpm:foo">>, + repo_name => <<"hexpm">>, repo_organization => <<"foo">>}, mocks => [first_auth] }, #{rebar_state := State} = Setup = setup_state(P, Config), @@ -293,7 +304,8 @@ org_key_revoke_test(Config) -> P = #{ command => #{provider => rebar3_hex_organization, args => ["key", "hexpm:foo", "revoke", "--key-name", "this-key"]}, app => #{name => "valid"}, - repo_config => #{repo => <<"hexpm:foo">>, name => <<"hexpm:foo">>}, + repo_config => #{repo => <<"hexpm:foo">>, name => <<"hexpm:foo">>, + repo_name => <<"hexpm">>, repo_organization => <<"foo">>}, mocks => [key_mutation] }, #{rebar_state := State, repo := _Repo} = Setup = setup_state(P, Config), @@ -306,7 +318,8 @@ org_key_revoke_all_test(Config) -> P = #{ command => #{provider => rebar3_hex_organization, args => ["key", "hexpm:foo", "revoke", "--all"]}, app => #{name => "valid"}, - repo_config => #{repo => <<"hexpm:foo">>, name => <<"hexpm:foo">>}, + repo_config => #{repo => <<"hexpm:foo">>, name => <<"hexpm:foo">>, + repo_name => <<"hexpm">>, repo_organization => <<"foo">>}, mocks => [key_mutation] }, #{rebar_state := State, repo := _Repo} = Setup = setup_state(P, Config), @@ -318,7 +331,8 @@ org_key_list_test(Config) -> P = #{ command => #{provider => rebar3_hex_organization, args => ["key", "hexpm:foo", "list"]}, app => #{name => "valid"}, - repo_config => #{repo => <<"hexpm:foo">>, name => <<"hexpm:foo">>}, + repo_config => #{repo => <<"hexpm:foo">>, name => <<"hexpm:foo">>, + repo_name => <<"hexpm">>, repo_organization => <<"foo">>}, mocks => [first_auth] }, #{rebar_state := State} = Setup = setup_state(P, Config), @@ -330,7 +344,8 @@ org_list_test(Config) -> P = #{ command => #{provider => rebar3_hex_organization, args => ["list"]}, app => #{name => "valid"}, - repo_config => #{repo => <<"hexpm:foo">>, name => <<"hexpm:foo">>}, + repo_config => #{repo => <<"hexpm:foo">>, name => <<"hexpm:foo">>, + repo_name => <<"hexpm">>, repo_organization => <<"foo">>}, mocks => [first_auth] }, #{rebar_state := State} = Setup = setup_state(P, Config), @@ -394,19 +409,14 @@ auth_unhandled_test(Config) -> meck:unload([hex_api_key]). auth_error_test(Config) -> - %% TODO: Revise hex_api_model and hex_db so that we don't need to meck this - meck:new([hex_api_key]), - meck:expect(hex_api_key, add, fun(_,_,_) -> {error, meh} end), - P = #{ - command => #{provider => rebar3_hex_user, args => ["auth"]}, + command => #{provider => rebar3_hex_user, args => ["auth", "--browser", "false"]}, app => #{name => "valid"}, - mocks => [first_auth] + mocks => [oauth_auth_error] }, #{rebar_state := State} = setup_state(P, Config), - ExpErr = {error,{rebar3_hex_user,{generate_key,{error, meh}}}}, - ?assertError(ExpErr, rebar3_hex_user:do(State)), - meck:unload([hex_api_key]). + ExpErr = {error, {rebar3_hex_user, {auth_failed, {device_auth_failed, 401, #{<<"message">> => <<"unauthorized">>}}}}}, + ?assertError(ExpErr, rebar3_hex_user:do(State)). whoami_test(Config) -> P = #{ @@ -417,12 +427,23 @@ whoami_test(Config) -> #{rebar_state := State} = setup_state(P, Config), ?assertMatch({ok, State}, rebar3_hex_user:do(State)). +whoami_not_authed_test(Config) -> + P = #{ + command => #{provider => rebar3_hex_user, args => ["whoami"]}, + app => #{name => "valid"}, + mocks => [], + repo_config => #{api_key => undefined} + }, + #{rebar_state := State} = setup_state(P, Config), + ?assertMatch({ok, State}, rebar3_hex_user:do(State)). + whoami_unknown_test(Config) -> P = #{ command => #{provider => rebar3_hex_user, args => ["whoami"]}, app => #{name => "valid"}, mocks => [whoami], - repo_config => #{read_key => <<"eh?">>} + %% Use api_key that the mock server will reject with "huh?" + repo_config => #{api_key => <<"eh?">>} }, #{rebar_state := State} = Setup = setup_state(P, Config), expects_parent_repos(Setup), @@ -452,18 +473,6 @@ whoami_error_test(Config) -> ExpErr = {error,{rebar3_hex_user,{whoami,{error, meh}}}}, ?assertError(ExpErr, rebar3_hex_user:do(State)). -whoami_not_authed_test(Config) -> - meck:expect(hex_api_user, me, fun(_) -> {error, meh} end), - P = #{ - command => #{provider => rebar3_hex_user, args => ["whoami"]}, - app => #{name => "valid"}, - mocks => [whoami], - repo_config => #{read_key => undefined} - }, - #{rebar_state := State} = Setup = setup_state(P, Config), - expects_parent_repos(Setup), - ?assertError({error, {rebar3_hex_user,not_authenticated}}, rebar3_hex_user:do(State)). - reset_password_test(Config) -> P = #{ command => #{provider => rebar3_hex_user, args => ["reset_password", "account"]}, @@ -513,10 +522,18 @@ deauth_test(Config) -> P = #{ command => #{provider => rebar3_hex_user, args => ["deauth"]}, app => #{name => "valid"}, - mocks => [deauth] + mocks => [oauth_deauth] }, - #{rebar_state := State} = Setup = setup_state(P, Config), - expects_repo_config(Setup), + #{rebar_state := State} = setup_state(P, Config), + ?assertMatch({ok, State}, rebar3_hex_user:do(State)). + +deauth_not_authenticated_test(Config) -> + P = #{ + command => #{provider => rebar3_hex_user, args => ["deauth"]}, + app => #{name => "valid"}, + mocks => [oauth_deauth_not_authenticated] + }, + #{rebar_state := State} = setup_state(P, Config), ?assertMatch({ok, State}, rebar3_hex_user:do(State)). build_package_test(Config) -> @@ -905,7 +922,8 @@ publish_org_test(Config) -> command => #{provider => rebar3_hex_publish, args => ["-r", "hexpm:valid"]}, app => #{name => "valid"}, mocks => [publish], - repo_config => #{repo => <<"hexpm:valid">>, name => <<"hexpm:valid">>} + repo_config => #{repo => <<"hexpm:valid">>, name => <<"hexpm:valid">>, + repo_name => <<"hexpm">>, repo_organization => <<"valid">>} }, #{rebar_state := State} = Setup = setup_state(P, Config), expects_repo_config(Setup), @@ -923,11 +941,13 @@ publish_org_non_hexpm_test(Config) -> ?assertMatch({ok, State}, rebar3_hex_publish:do(State)). publish_org_error_test(Config) -> + %% Test that publishing to a repo with a non-existent parent fails P = #{ - command => #{provider => rebar3_hex_publish, args => ["-r", "hexpm:bar"]}, + command => #{provider => rebar3_hex_publish, args => ["-r", "nonexistent:bar"]}, app => #{name => "valid"}, mocks => [], - repo_config => #{repo => <<"hexpm:foo">>, name => <<"hexpm:foo">>} + repo_config => #{repo => <<"hexpm:foo">>, name => <<"hexpm:foo">>, + repo_name => <<"hexpm">>, repo_organization => <<"foo">>} }, #{rebar_state := State} = Setup = setup_state(P, Config), expects_update_auth_config(Setup), @@ -940,14 +960,15 @@ publish_org_error_test(Config) -> expects_prompts(Exps), expects_output(default_publish_io(Setup)), - ExpError = {error,{rebar3_hex_publish,{not_valid_repo,"hexpm:bar"}}}, + ExpError = {error,{rebar3_hex_publish,{not_valid_repo,"nonexistent:bar"}}}, ?assertError(ExpError, rebar3_hex_publish:do(State)). publish_org_requires_repo_arg_test(Config) -> P = #{command => #{provider => rebar3_hex_publish, args => []}, app => #{name => "valid"}, mocks => [], - repo_config => #{repo => <<"hexpm:valid">>, name => <<"hexpm:valid">>} + repo_config => #{repo => <<"hexpm:valid">>, name => <<"hexpm:valid">>, + repo_name => <<"hexpm">>, repo_organization => <<"valid">>} }, #{rebar_state := State} = Setup = setup_state(P, Config), expects_update_auth_config(Setup), @@ -966,19 +987,26 @@ publish_error_test(Config) -> command => #{provider => rebar3_hex_publish, args => []}, app => #{name => "valid"}, mocks => [publish], - repo_config => #{write_key => undefined} + repo_config => #{api_key => <<"server_error_key">>} }, #{rebar_state := State} = Setup = setup_state(P, Config), expects_repo_config(Setup), - ?assertError({error,{rebar3_hex_publish,{get_hex_config, no_write_key}}}, rebar3_hex_publish:do(State)). + Exp = {error, + {rebar3_hex_publish, + {publish_package, + <<"valid">>, + "0.1.0", + {error, + #{<<"message">> => + <<"internal server error">>}}}}}, + ?assertError(Exp, rebar3_hex_publish:do(State)). publish_unauthorized_test(Config) -> - WriteKey = rebar3_hex_user:encrypt_write_key(<<"mr_pockets">>, <<"special_shoes">>, <<"unauthorized">>), P = #{ - command => #{provider => rebar3_hex_publish, args => ["-r", "hexpm:valid"]}, + command => #{provider => rebar3_hex_publish, args => []}, app => #{name => "valid"}, mocks => [publish], - repo_config => #{write_key => WriteKey, repo => <<"hexpm:valid">>, name => <<"hexpm:valid">>} + repo_config => #{api_key => <<"read_only_key">>} }, #{rebar_state := State} = Setup = setup_state(P, Config), expects_repo_config(Setup), @@ -1112,6 +1140,117 @@ owner_list_test(Config) -> create_user(?default_username, ?default_password, ?default_email, Repo), ?assertMatch({ok, State}, rebar3_hex_owner:do(State)). +%% Repo command tests + +repo_no_args_test(Config) -> + P = #{ + command => #{provider => rebar3_hex_repo, args => []}, + app => #{name => "valid"}, + mocks => [repo] + }, + #{rebar_state := State} = setup_state(P, Config), + ?assertMatch({ok, _}, rebar3_hex_repo:do(State)). + +repo_unknown_subcommand_test(Config) -> + P = #{ + command => #{provider => rebar3_hex_repo, args => ["unknown"]}, + app => #{name => "valid"}, + mocks => [] + }, + #{rebar_state := State} = setup_state(P, Config), + ?assertError({error, {rebar3_hex_repo, {unknown_subcommand, "unknown"}}}, rebar3_hex_repo:do(State)). + +repo_add_missing_name_test(Config) -> + P = #{ + command => #{provider => rebar3_hex_repo, args => ["add"]}, + app => #{name => "valid"}, + mocks => [] + }, + #{rebar_state := State} = setup_state(P, Config), + ?assertError({error, {rebar3_hex_repo, missing_repo_name}}, rebar3_hex_repo:do(State)). + +repo_add_missing_url_test(Config) -> + P = #{ + command => #{provider => rebar3_hex_repo, args => ["add", "myrepo"]}, + app => #{name => "valid"}, + mocks => [] + }, + #{rebar_state := State} = setup_state(P, Config), + ?assertError({error, {rebar3_hex_repo, {missing_url, "myrepo"}}}, rebar3_hex_repo:do(State)). + +repo_remove_hexpm_test(Config) -> + P = #{ + command => #{provider => rebar3_hex_repo, args => ["remove", "hexpm"]}, + app => #{name => "valid"}, + mocks => [] + }, + #{rebar_state := State} = setup_state(P, Config), + ?assertError({error, {rebar3_hex_repo, cannot_remove_hexpm}}, rebar3_hex_repo:do(State)). + +repo_list_test(Config) -> + P = #{ + command => #{provider => rebar3_hex_repo, args => ["list"]}, + app => #{name => "valid"}, + mocks => [repo] + }, + #{rebar_state := State} = setup_state(P, Config), + ?assertMatch({ok, _}, rebar3_hex_repo:do(State)). + +repo_add_test(Config) -> + P = #{ + command => #{provider => rebar3_hex_repo, args => ["add", "myrepo", "http://127.0.0.1:3000"]}, + app => #{name => "valid"}, + mocks => [repo] + }, + #{rebar_state := State} = setup_state(P, Config), + ?assertMatch({ok, _}, rebar3_hex_repo:do(State)). + +repo_add_with_auth_key_test(Config) -> + P = #{ + command => #{provider => rebar3_hex_repo, args => ["add", "myrepo2", "http://127.0.0.1:3000", "--auth-key", "secret123"]}, + app => #{name => "valid"}, + mocks => [repo] + }, + #{rebar_state := State} = setup_state(P, Config), + ?assertMatch({ok, _}, rebar3_hex_repo:do(State)). + +repo_set_test(Config) -> + P = #{ + command => #{provider => rebar3_hex_repo, args => ["set", "hexpm", "--auth-key", "newkey"]}, + app => #{name => "valid"}, + mocks => [repo] + }, + #{rebar_state := State} = setup_state(P, Config), + ?assertMatch({ok, _}, rebar3_hex_repo:do(State)). + +repo_show_test(Config) -> + P = #{ + command => #{provider => rebar3_hex_repo, args => ["show", "hexpm"]}, + app => #{name => "valid"}, + mocks => [repo] + }, + #{rebar_state := State} = setup_state(P, Config), + ?assertMatch({ok, _}, rebar3_hex_repo:do(State)). + +repo_remove_test(Config) -> + %% Add a repo, then remove it using the same state + P = #{ + command => #{provider => rebar3_hex_repo, args => ["add", "removeme", "http://127.0.0.1:3000"]}, + app => #{name => "valid"}, + mocks => [repo] + }, + #{rebar_state := State1} = setup_state(P, Config), + {ok, State2} = rebar3_hex_repo:do(State1), + + %% Now remove it - reparse args for the remove command + State3 = rebar_state:command_args(State2, ["remove", "removeme"]), + {ok, State4} = rebar3_hex_repo:init(State3), + Provider = providers:get_provider_by_module(rebar3_hex_repo, rebar_state:providers(State4)), + Opts = providers:opts(Provider) ++ rebar3:global_option_spec_list(), + {ok, ParsedArgs} = getopt:parse(Opts, ["remove", "removeme"]), + State5 = rebar_state:command_parsed_args(State4, ParsedArgs), + ?assertMatch({ok, _}, rebar3_hex_repo:do(State5)). + bad_command_test(Config) -> P = #{command => #{provider => rebar3_hex_user, args => ["bad_command"]}, app => #{name => "valid"}, mocks => [deauth]}, @@ -1137,16 +1276,16 @@ revert_invalid_ver_test(Config) -> setup_state(P, Config) -> Params = maps:merge(?default_params, P), #{mocks := Mocks, - username := Username, - password := Password, + username := _Username, + password := _Password, password_confirmation := _PasswordConfirm, key_phrase := KeyPhrase, email := _Email} = Params, - WriteKey = maps:get(write_key, Params, rebar3_hex_user:encrypt_write_key(Username, Password, KeyPhrase)), + ApiKey = maps:get(api_key, Params, KeyPhrase), DefRepoConfig = test_utils:default_config(), - ParamRepoConfig = maps:get(repo_config, Params, DefRepoConfig), - RepoConfig = maps:merge(DefRepoConfig#{write_key => WriteKey}, ParamRepoConfig), + ParamRepoConfig = maps:get(repo_config, Params, DefRepoConfig), + RepoConfig = maps:merge(DefRepoConfig#{api_key => ApiKey}, ParamRepoConfig), Repo = test_utils:repo_config(RepoConfig), StubSpec = get_stub_spec(P), @@ -1197,7 +1336,15 @@ setup_state(P, Config) -> lists:foreach(fun(W) -> setup_mocks_for(W, Setup) end, Mocks), #{command := #{provider := Provider, args := Args}} = Setup, - {ok, State1} = test_utils:mock_command(Provider, Args, [{repos, [Repo]}, {doc, edoc}], State), + %% For org repos, also include the parent repo so rebar_hex_auth can look it up + Repos = case Repo of + #{repo_name := ParentName, repo_organization := _Org} -> + ParentRepo = test_utils:repo_config(#{name => ParentName, repo_name => ParentName}), + [Repo, ParentRepo]; + _ -> + [Repo] + end, + {ok, State1} = test_utils:mock_command(Provider, Args, [{repos, Repos}, {doc, edoc}], State), Setup#{rebar_state => State1}. @@ -1371,7 +1518,56 @@ setup_mocks_for(register, #{email := Email, repo := Repo} = Setup) -> setup_mocks_for(register_existing, Setup) -> expects_registration_output(), expect_local_password_prompt(Setup), - expects_user_registration_prompts(Setup). + expects_user_registration_prompts(Setup); + +setup_mocks_for(repo, _Setup) -> + %% Repo commands use rebar3_hex_io:say for output + %% Allow any output since repo commands have various messages + meck:expect(rebar3_hex_io, say, fun(_) -> ok end), + meck:expect(rebar3_hex_io, say, fun(_, _) -> ok end), + %% Mock auth_config to return an in-memory map + %% We use the process dictionary to maintain state between calls + case get(repo_auth_config) of + undefined -> put(repo_auth_config, #{}); + _ -> ok + end, + meck:expect(rebar3_hex_config, auth_config, fun(_State) -> + case get(repo_auth_config) of + undefined -> #{}; + Config -> Config + end + end), + meck:expect(rebar3_hex_config, update_repo_auth_config, fun(RepoConfig, RepoName, _State) -> + Current = case get(repo_auth_config) of + undefined -> #{}; + C -> C + end, + Existing = maps:get(RepoName, Current, #{}), + Updated = maps:merge(Existing, RepoConfig), + put(repo_auth_config, maps:put(RepoName, Updated, Current)), + ok + end), + meck:expect(rebar3_hex_config, remove_from_auth_config, fun(RepoName, _State) -> + Current = case get(repo_auth_config) of + undefined -> #{}; + C -> C + end, + put(repo_auth_config, maps:remove(RepoName, Current)), + ok + end); + +setup_mocks_for(oauth_auth, _Setup) -> + ok; + +setup_mocks_for(oauth_auth_error, _Setup) -> + %% OAuth auth error - set flag so mock server returns an error + hex_db:set_oauth_device(<<"force_error">>, error); + +setup_mocks_for(oauth_deauth, _Setup) -> + ok; + +setup_mocks_for(oauth_deauth_not_authenticated, _Setup) -> + ok. default_publish_io(#{selected_app := #{name := AppName}, repo := #{name := RepoName}}) -> @@ -1398,27 +1594,39 @@ expects_registration_output() -> ++ " policies and terms of service found at https://hex.pm/policies\n", expects_output([ExpectedInfo, "Registering..."]). -expects_registration_confirmation_output(RepoName, Email) -> +expects_registration_confirmation_output(_RepoName, Email) -> ReqInfo = "You are required to confirm your email to access your account, " ++ "a confirmation email has been sent to ~s", - TokenInfo = "Then run `rebar3 hex auth -r ~ts` to create and configure api tokens locally.", - expects_output([{TokenInfo, [RepoName]}, {ReqInfo, [Email]}]). - -expects_repo_config(#{repo := Repo}) -> - meck:expect(rebar3_hex_config, all_repos, fun(_) -> [Repo] end), - meck:expect(rebar3_hex_config, repo, fun(_) -> {ok, Repo} end), - meck:expect(rebar3_hex_config, repo, fun(_, <<"hexpm">>) -> {ok, test_utils:default_config()} end). + TokenInfo = "Then run `rebar3 hex user auth` to authenticate.", + expects_output([{TokenInfo, []}, {ReqInfo, [Email]}]). + +expects_repo_config(#{repo := Repo, rebar_state := State}) -> + ApiKey = maps:get(api_key, Repo, <<"key">>), + %% Write auth config for rebar_hex_auth integration + %% For org repos, rebar3_hex_organization calls with_api using the PARENT repo name, + %% so we need to set up auth for the parent (e.g., "hexpm"), not the full org name + AuthRepoName = case Repo of + #{repo_name := RN} when is_binary(RN) -> + RN; + #{name := Name} -> + Name + end, + rebar_hex_repos:update_auth_config(#{AuthRepoName => #{api_key => ApiKey}}, State); +expects_repo_config(#{rebar_state := _State}) -> + %% No mocking needed - repos are already in the state + ok; +expects_repo_config(#{}) -> + %% No state available, nothing to configure + ok. expects_parent_repos(#{repo := Repo}) -> meck:expect(rebar3_hex_config, parent_repos, fun(_) -> [Repo] end). -expects_update_auth_config( #{username := Username, repo := Repo}) -> - BinUsername = rebar_utils:to_binary(Username), +expects_update_auth_config(#{repo := Repo}) -> Fun = fun(Cfg, State) -> Rname = maps:get(name, Repo), - Skey = maps:get(repo_key, Repo), [Rname] = maps:keys(Cfg), - #{repo_key := Skey, username := BinUsername} = maps:get(Rname, Cfg), + _ = maps:get(Rname, Cfg), {ok, State} end, meck:expect(rebar3_hex_config, update_auth_config, Fun). diff --git a/test/rebar3_hex_organization_SUITE.erl b/test/rebar3_hex_organization_SUITE.erl new file mode 100644 index 00000000..4cc3f21c --- /dev/null +++ b/test/rebar3_hex_organization_SUITE.erl @@ -0,0 +1,107 @@ +-module(rebar3_hex_organization_SUITE). + +-compile(export_all). +-include_lib("common_test/include/ct.hrl"). +-include_lib("eunit/include/eunit.hrl"). + +%%%%%%%%%%%%%%%%%% +%%% CT hooks %%% +%%%%%%%%%%%%%%%%%% + +all() -> + [ + %% format_error tests + format_error_no_repo_test, + format_error_auth_no_key_test, + format_error_auth_binary_test, + format_error_auth_map_test, + format_error_generate_key_binary_test, + format_error_generate_key_map_test, + format_error_key_generate_binary_test, + format_error_key_generate_map_test, + format_error_key_revoke_all_binary_test, + format_error_key_revoke_all_map_test, + format_error_key_list_binary_test, + format_error_key_list_map_test, + format_error_bad_command_test, + format_error_not_a_valid_repo_name_test, + format_error_get_parent_repo_and_org_name_test, + format_error_get_repo_by_name_test, + format_error_unknown_test + ]. + +%%%%%%%%%%%%%%%%%% +%%% Test Cases %%% +%%%%%%%%%%%%%%%%%% + +%% format_error tests for organization auth/deauth related errors + +format_error_no_repo_test(_Config) -> + Result = rebar3_hex_organization:format_error(no_repo), + ?assertEqual("Authenticate and generate commands require repository name as argument", Result). + +format_error_auth_no_key_test(_Config) -> + Result = rebar3_hex_organization:format_error(auth_no_key), + ?assertEqual("Repo authenticate command requires key", Result). + +format_error_auth_binary_test(_Config) -> + Result = rebar3_hex_organization:format_error({auth, <<"authentication error">>}), + ?assertEqual("Error authenticating organization : authentication error", lists:flatten(Result)). + +format_error_auth_map_test(_Config) -> + Result = rebar3_hex_organization:format_error({auth, #{<<"error">> => <<"bad credentials">>}}), + ?assertMatch("Error authenticating organization : " ++ _, lists:flatten(Result)). + +format_error_generate_key_binary_test(_Config) -> + Result = rebar3_hex_organization:format_error({generate_key, <<"key generation failed">>}), + ?assertEqual("Error generating organization key: key generation failed", lists:flatten(Result)). + +format_error_generate_key_map_test(_Config) -> + Result = rebar3_hex_organization:format_error({generate_key, #{<<"error">> => <<"invalid permissions">>}}), + ?assertMatch("Error generating organization key: " ++ _, lists:flatten(Result)). + +format_error_key_generate_binary_test(_Config) -> + Result = rebar3_hex_organization:format_error({key_generate, <<"key generate error">>}), + ?assertEqual("Error generating organization key: key generate error", lists:flatten(Result)). + +format_error_key_generate_map_test(_Config) -> + Result = rebar3_hex_organization:format_error({key_generate, #{<<"error">> => <<"map error">>}}), + ?assertMatch("Error generating organization key: " ++ _, lists:flatten(Result)). + +format_error_key_revoke_all_binary_test(_Config) -> + Result = rebar3_hex_organization:format_error({key_revoke_all, <<"revoke all failed">>}), + ?assertEqual("Error revoking all organization keys: revoke all failed", lists:flatten(Result)). + +format_error_key_revoke_all_map_test(_Config) -> + Result = rebar3_hex_organization:format_error({key_revoke_all, #{<<"error">> => <<"revoke error">>}}), + ?assertMatch("Error revoking all organization keys: " ++ _, lists:flatten(Result)). + +format_error_key_list_binary_test(_Config) -> + Result = rebar3_hex_organization:format_error({key_list, <<"list failed">>}), + ?assertEqual("Error listing organization keys: list failed", lists:flatten(Result)). + +format_error_key_list_map_test(_Config) -> + Result = rebar3_hex_organization:format_error({key_list, #{<<"error">> => <<"list error">>}}), + ?assertMatch("Error listing organization keys: " ++ _, lists:flatten(Result)). + +format_error_bad_command_test(_Config) -> + Result = rebar3_hex_organization:format_error(bad_command), + ?assertMatch("Invalid arguments, expected one of:" ++ _, lists:flatten(Result)). + +format_error_not_a_valid_repo_name_test(_Config) -> + Result = rebar3_hex_organization:format_error(not_a_valid_repo_name), + Expected = "Invalid organization repository: organization name arguments must be given as a fully qualified " + "repository name (i.e, hexpm:my_org)", + ?assertEqual(Expected, Result). + +format_error_get_parent_repo_and_org_name_test(_Config) -> + Result = rebar3_hex_organization:format_error({get_parent_repo_and_org_name, {error, not_found}, <<"hexpm:myorg">>}), + ?assertMatch("Error getting the parent repo for hexpm:myorg" ++ _, lists:flatten(Result)). + +format_error_get_repo_by_name_test(_Config) -> + Result = rebar3_hex_organization:format_error({get_repo_by_name, {error, {not_valid_repo, <<"hexpm">>}}}), + ?assertMatch("You do not appear to be authenticated as a user to the hexpm repository." ++ _, lists:flatten(Result)). + +format_error_unknown_test(_Config) -> + Result = rebar3_hex_organization:format_error(unknown_error), + ?assertEqual("An unknown error was encountered. Run with DIAGNOSTIC=1 for more details.", Result). \ No newline at end of file diff --git a/test/rebar3_hex_publish_SUITE.erl b/test/rebar3_hex_publish_SUITE.erl index 6e11f561..60ced23a 100644 --- a/test/rebar3_hex_publish_SUITE.erl +++ b/test/rebar3_hex_publish_SUITE.erl @@ -27,10 +27,6 @@ format_error_test(_Config) -> {{no_license, myapp}, <<"myapp.app.src : missing or empty licenses property">>}, {{has_maintainers, myapp}, <<"myapp.app.src : deprecated field maintainers found">>}, {{has_contributors, myapp}, <<"myapp.app.src : deprecated field contributors found">>}, - {{get_hex_config, no_write_key}, - <<"No write key found for user. Be sure to authenticate first with: rebar3 hex user auth">>}, - - { {create_package, {error, "some error"}}, <<"Error creating package : some error">> diff --git a/test/rebar3_hex_repo_SUITE.erl b/test/rebar3_hex_repo_SUITE.erl new file mode 100644 index 00000000..cb9a4ed1 --- /dev/null +++ b/test/rebar3_hex_repo_SUITE.erl @@ -0,0 +1,84 @@ +-module(rebar3_hex_repo_SUITE). + +-compile(export_all). +-include_lib("common_test/include/ct.hrl"). +-include_lib("eunit/include/eunit.hrl"). + +%%%%%%%%%%%%%%%%%% +%%% CT hooks %%% +%%%%%%%%%%%%%%%%%% + +all() -> + [ + format_error_unknown_subcommand_test, + format_error_missing_repo_name_test, + format_error_missing_url_test, + format_error_repo_exists_test, + format_error_repo_not_found_test, + format_error_invalid_public_key_test, + format_error_fetch_public_key_failed_status_test, + format_error_fetch_public_key_failed_reason_test, + format_error_public_key_mismatch_test, + format_error_invalid_fingerprint_format_test, + format_error_fetch_public_key_requires_url_test, + format_error_cannot_remove_hexpm_test, + format_error_unknown_test + ]. + +%%%%%%%%%%%%%%%%%% +%%% Test Cases %%% +%%%%%%%%%%%%%%%%%% + +format_error_unknown_subcommand_test(_Config) -> + Result = rebar3_hex_repo:format_error({unknown_subcommand, "foo"}), + ?assertEqual("Unknown subcommand: foo", lists:flatten(Result)). + +format_error_missing_repo_name_test(_Config) -> + Result = rebar3_hex_repo:format_error(missing_repo_name), + ?assertEqual("Missing repository name", Result). + +format_error_missing_url_test(_Config) -> + Result = rebar3_hex_repo:format_error({missing_url, "myrepo"}), + ?assertEqual("Missing URL for repository: myrepo", lists:flatten(Result)). + +format_error_repo_exists_test(_Config) -> + Result = rebar3_hex_repo:format_error({repo_exists, "myrepo"}), + ?assertEqual("Repository already exists: myrepo. Use 'hex repo set' to update.", + lists:flatten(Result)). + +format_error_repo_not_found_test(_Config) -> + Result = rebar3_hex_repo:format_error({repo_not_found, "myrepo"}), + ?assertEqual("Repository not found: myrepo", lists:flatten(Result)). + +format_error_invalid_public_key_test(_Config) -> + Result = rebar3_hex_repo:format_error({invalid_public_key, "/path/to/key", enoent}), + ?assertMatch("Failed to read public key from /path/to/key: " ++ _, lists:flatten(Result)). + +format_error_fetch_public_key_failed_status_test(_Config) -> + Result = rebar3_hex_repo:format_error({fetch_public_key_failed, 404}), + ?assertEqual("Failed to fetch public key: HTTP 404", lists:flatten(Result)). + +format_error_fetch_public_key_failed_reason_test(_Config) -> + Result = rebar3_hex_repo:format_error({fetch_public_key_failed, timeout}), + ?assertMatch("Failed to fetch public key: " ++ _, lists:flatten(Result)). + +format_error_public_key_mismatch_test(_Config) -> + Result = rebar3_hex_repo:format_error({public_key_mismatch, "SHA256:expected", "SHA256:actual"}), + Expected = "Public key fingerprint mismatch!\n Expected: SHA256:expected\n Got: SHA256:actual", + ?assertEqual(Expected, lists:flatten(Result)). + +format_error_invalid_fingerprint_format_test(_Config) -> + Result = rebar3_hex_repo:format_error({invalid_fingerprint_format, "badformat"}), + ?assertMatch("Invalid fingerprint format: badformat" ++ _, lists:flatten(Result)). + +format_error_fetch_public_key_requires_url_test(_Config) -> + Result = rebar3_hex_repo:format_error(fetch_public_key_requires_url), + ?assertEqual("Cannot fetch public key: repository URL not set. Use --url or add URL first.", Result). + +format_error_cannot_remove_hexpm_test(_Config) -> + Result = rebar3_hex_repo:format_error(cannot_remove_hexpm), + ?assertEqual("Cannot remove the default hexpm repository", Result). + +format_error_unknown_test(_Config) -> + Result = rebar3_hex_repo:format_error(unknown_error), + ?assertEqual("An unknown error was encountered. Run with DIAGNOSTIC=1 for more details.", Result). diff --git a/test/rebar3_hex_user_SUITE.erl b/test/rebar3_hex_user_SUITE.erl index 6dd79697..e007a334 100644 --- a/test/rebar3_hex_user_SUITE.erl +++ b/test/rebar3_hex_user_SUITE.erl @@ -5,38 +5,77 @@ -include_lib("eunit/include/eunit.hrl"). all() -> - [format_error_test]. + [ + format_error_not_authenticated_test, + format_error_whoami_test, + format_error_registration_failure_test, + format_error_generate_key_test, + format_error_passwords_do_not_match_test, + format_error_reset_account_password_test, + format_error_bad_command_test, + format_error_input_required_test, + format_error_key_revoke_test, + format_error_key_revoke_all_test, + format_error_key_list_test, + format_error_auth_failed_test, + format_error_revoke_failed_test, + format_error_unknown_test + ]. -encrypt_decrypt_write_key_test(_Config) -> - WriteKey = <<"abc1234">>, +%% format_error tests for auth/deauth related errors - Username = <<"user">>, - LocalPassword = <<"password">>, - - WriteKeyEncrypted = rebar3_hex_user:encrypt_write_key(Username, LocalPassword, WriteKey), - - ?assertError({error,{rebar3_hex_user,bad_local_password}}, - rebar3_hex_user:decrypt_write_key(Username, <<"wrong password">>, WriteKeyEncrypted)), - - ?assertEqual(WriteKey, rebar3_hex_user:decrypt_write_key(Username, LocalPassword, WriteKeyEncrypted)). - -format_error_test(_Config) -> +format_error_not_authenticated_test(_Config) -> ?assertEqual(<<"Not authenticated as any user currently for this repository">>, - list_to_bitstring(rebar3_hex_user:format_error(not_authenticated))), + list_to_bitstring(rebar3_hex_user:format_error(not_authenticated))). + +format_error_whoami_test(_Config) -> ?assertEqual(<<"Fetching currently authenticated user failed: eh">>, - list_to_bitstring(rebar3_hex_user:format_error({whoami, <<"eh">>}))), - ?assertEqual(<<"Failure to decrypt write key: bad local password">>, - list_to_bitstring(rebar3_hex_user:format_error(bad_local_password))), + list_to_bitstring(rebar3_hex_user:format_error({whoami, <<"eh">>}))). + +format_error_registration_failure_test(_Config) -> ?assertEqual(<<"Registration of user failed: foo bar">>, - list_to_bitstring(rebar3_hex_user:format_error({registration_failure, #{<<"foo">> => <<"bar">>}}))), + list_to_bitstring(rebar3_hex_user:format_error({registration_failure, #{<<"foo">> => <<"bar">>}}))). + +format_error_generate_key_test(_Config) -> ?assertEqual(<<"Failure generating authentication tokens: eh">>, - list_to_bitstring(rebar3_hex_user:format_error({generate_key, <<"eh">>}))), + list_to_bitstring(rebar3_hex_user:format_error({generate_key, <<"eh">>}))). + +format_error_passwords_do_not_match_test(_Config) -> ?assertEqual(<<"Password confirmation failed. The passwords must match.">>, - list_to_bitstring(rebar3_hex_user:format_error(passwords_do_not_match))), - ?assertEqual(<<"Local passwords can not exceed 32 characters.">>, - list_to_bitstring(rebar3_hex_user:format_error(local_password_too_big))), + list_to_bitstring(rebar3_hex_user:format_error(passwords_do_not_match))). + +format_error_reset_account_password_test(_Config) -> ?assertEqual(<<"Error reseting account password: eh?">>, - list_to_bitstring(rebar3_hex_user:format_error({reset_account_password, <<"eh?">>}))), + list_to_bitstring(rebar3_hex_user:format_error({reset_account_password, <<"eh?">>}))). + +format_error_bad_command_test(_Config) -> ?assertMatch(<<"Invalid arguments, expected one of:", _Rest/binary>>, - list_to_bitstring(rebar3_hex_user:format_error(bad_command))), - ?assertEqual("An unknown error was encountered. Run with DIAGNOSTIC=1 for more details.", rebar3_hex_user:format_error('eh?')). + list_to_bitstring(rebar3_hex_user:format_error(bad_command))). + +format_error_input_required_test(_Config) -> + Result = rebar3_hex_user:format_error({input_required, "Username"}), + ?assertMatch("The task you are attempting to run requires a Username. " ++ _, lists:flatten(Result)). + +format_error_key_revoke_test(_Config) -> + Result = rebar3_hex_user:format_error({key_revoke, {error, #{<<"status">> => 404}}}), + ?assertEqual("The key you tried to revoke was not found", lists:flatten(Result)). + +format_error_key_revoke_all_test(_Config) -> + Result = rebar3_hex_user:format_error({key_revoke_all, {error, #{<<"message">> => <<"error msg">>}}}), + ?assertEqual("Error revoking all keys : error msg", lists:flatten(Result)). + +format_error_key_list_test(_Config) -> + Result = rebar3_hex_user:format_error({key_list, {error, #{<<"message">> => <<"list error">>}}}), + ?assertEqual("Error listing keys : list error", lists:flatten(Result)). + +format_error_auth_failed_test(_Config) -> + Result = rebar3_hex_user:format_error({auth_failed, timeout}), + ?assertEqual("Authentication failed: timeout", lists:flatten(Result)). + +format_error_revoke_failed_test(_Config) -> + Result = rebar3_hex_user:format_error({revoke_failed, 500, <<"server error">>}), + ?assertMatch("Failed to revoke token (500): " ++ _, lists:flatten(Result)). + +format_error_unknown_test(_Config) -> + ?assertEqual("An unknown error was encountered. Run with DIAGNOSTIC=1 for more details.", + rebar3_hex_user:format_error('eh?')). diff --git a/test/support/hex_api_model.erl b/test/support/hex_api_model.erl index 34793f36..0946a18d 100644 --- a/test/support/hex_api_model.erl +++ b/test/support/hex_api_model.erl @@ -21,9 +21,89 @@ handle(Req, _Args) -> handle(Req#req.method, elli_request:path(Req), Req). -handle('GET', [<<"auth">>], Req) -> +handle('GET', [<<"auth">>], Req) -> respond_with(200, Req, #{}); +%% OAuth Device Authorization Flow endpoints +handle('POST', [<<"oauth">>, <<"device_authorization">>], Req) -> + %% Check if we should simulate an error + case hex_db:get_oauth_device(<<"force_error">>) of + error -> + respond_with(401, Req, #{<<"message">> => <<"unauthorized">>}); + _ -> + %% Store the device code in hex_db so we can track it + DeviceCode = <<"test_device_code_", (integer_to_binary(erlang:unique_integer([positive])))/binary>>, + UserCode = <<"TEST-CODE">>, + hex_db:set_oauth_device(DeviceCode, pending), + Res = #{ + <<"device_code">> => DeviceCode, + <<"user_code">> => UserCode, + <<"verification_uri">> => <<"http://127.0.0.1:3000/oauth/verify">>, + <<"verification_uri_complete">> => <<"http://127.0.0.1:3000/oauth/verify?code=", UserCode/binary>>, + <<"expires_in">> => 900, + <<"interval">> => 1 + }, + respond_with(200, Req, Res) + end; + +handle('POST', [<<"oauth">>, <<"token">>], Req) -> + Data = body_to_terms(Req), + case maps:get(<<"grant_type">>, Data, undefined) of + <<"urn:ietf:params:oauth:grant-type:device_code">> -> + DeviceCode = maps:get(<<"device_code">>, Data), + case hex_db:get_oauth_device(DeviceCode) of + authorized -> + %% User has authorized - return tokens + ExpiresAt = erlang:system_time(second) + 3600, + Res = #{ + <<"access_token">> => <<"test_access_token">>, + <<"refresh_token">> => <<"test_refresh_token">>, + <<"token_type">> => <<"bearer">>, + <<"expires_in">> => 3600, + <<"expires_at">> => ExpiresAt + }, + respond_with(200, Req, Res); + pending -> + %% Auto-authorize on first poll for testing convenience + hex_db:set_oauth_device(DeviceCode, authorized), + ExpiresAt = erlang:system_time(second) + 3600, + Res = #{ + <<"access_token">> => <<"test_access_token">>, + <<"refresh_token">> => <<"test_refresh_token">>, + <<"token_type">> => <<"bearer">>, + <<"expires_in">> => 3600, + <<"expires_at">> => ExpiresAt + }, + respond_with(200, Req, Res); + undefined -> + respond_with(400, Req, #{<<"error">> => <<"invalid_grant">>}) + end; + <<"refresh_token">> -> + %% Handle token refresh + ExpiresAt = erlang:system_time(second) + 3600, + Res = #{ + <<"access_token">> => <<"refreshed_access_token">>, + <<"refresh_token">> => <<"refreshed_refresh_token">>, + <<"token_type">> => <<"bearer">>, + <<"expires_in">> => 3600, + <<"expires_at">> => ExpiresAt + }, + respond_with(200, Req, Res); + _ -> + respond_with(400, Req, #{<<"error">> => <<"unsupported_grant_type">>}) + end; + +handle('POST', [<<"oauth">>, <<"revoke">>], Req) -> + %% Token revocation - always succeeds + respond_with(200, Req, #{}); + +%% Endpoint to simulate user authorizing the device (for tests to call) +handle('POST', [<<"oauth">>, <<"authorize_device">>], Req) -> + Data = body_to_terms(Req), + DeviceCode = maps:get(<<"device_code">>, Data), + hex_db:set_oauth_device(DeviceCode, authorized), + respond_with(200, Req, #{<<"status">> => <<"authorized">>}); + handle('GET', [<<"orgs">>, <<"foo">>, <<"keys">>], Req) -> Res = [ #{<<"name">> => <<"key1">>, @@ -56,6 +136,14 @@ handle('POST', [<<"packages">>, _Name, <<"releases">>, _Version, <<"docs">>], Re respond_with(401, Req, #{}) end; +handle('POST', [<<"repos">>, _Repo, <<"packages">>, _Name, <<"releases">>, _Version, <<"docs">>], Req) -> + case authenticate(Req) of + {ok, #{username := _Username, email := _Email}} -> + respond_with(201, Req, <<>>); + error -> + respond_with(401, Req, #{}) + end; + handle('GET', [<<"packages">>, _Name, <<"owners">>], Req) -> case authenticate(Req) of {ok, #{username := _Username, email := _Email}} -> @@ -288,6 +376,8 @@ handle_publish(Req) -> respond_with(201, Req, Res); unauthorized -> respond_with(403, Req, #{<<"message">> => <<"account not authorized for this action">>}); + server_error -> + respond_with(500, Req, #{<<"message">> => <<"internal server error">>}); error -> respond_with(401, Req, #{}) end. @@ -334,12 +424,23 @@ to(T, Term) when T =:= json andalso T =:= hex_json -> authenticate(Req) -> case elli_request:get_header(<<"Authorization">>, Req) of + <<"read_only_key">> -> + unauthorized; + <<"server_error_key">> -> + server_error; Key when Key =:= <<"key">> orelse Key =:= <<"123">> -> {ok, #{username => <<"mr_pockets">>, email => <<"foo@bar.baz">>, organization => <<"hexpm">>, source => key, key => <<"key">>}}; + <<"Bearer ", Token/binary>> when Token =:= <<"test_access_token">> orelse + Token =:= <<"refreshed_access_token">> -> + {ok, #{username => <<"mr_pockets">>, + email => <<"foo@bar.baz">>, + organization => <<"hexpm">>, + source => oauth, + token => Token}}; <<"unauthorized">> -> unauthorized; _ -> diff --git a/test/support/hex_db.erl b/test/support/hex_db.erl index e906251d..dfad90c1 100644 --- a/test/support/hex_db.erl +++ b/test/support/hex_db.erl @@ -4,7 +4,7 @@ -export([start_link/0, stop/1]). --export([add_user/1]). +-export([add_user/1, set_oauth_device/2, get_oauth_device/1]). -export([handle_call/3, handle_info/2, handle_cast/2, init/1]). @@ -33,6 +33,17 @@ add_user(#{<<"username">> := Username, <<"email">> := Email} = User) -> false -> {error, user_exists} end. +%% OAuth device authorization tracking +set_oauth_device(DeviceCode, Status) -> + ets:insert(?MODULE, {{oauth_device, DeviceCode}, Status}), + ok. + +get_oauth_device(DeviceCode) -> + case ets:lookup(?MODULE, {oauth_device, DeviceCode}) of + [{{oauth_device, DeviceCode}, Status}] -> Status; + [] -> undefined + end. + % Server init([]) -> _Tid = ets:new(?MODULE, [named_table, public, {write_concurrency, true}]), diff --git a/test/support/test_utils.erl b/test/support/test_utils.erl index fbb3ce0f..208c0c51 100644 --- a/test/support/test_utils.erl +++ b/test/support/test_utils.erl @@ -18,12 +18,9 @@ J1i2xWFndWa6nfFnRxZmCStCOZWYYPlaxr+FZceFbpMwzTNs4g3d4tLNUcbKAIH4 api_url => <<"http://127.0.0.1:3000">>, repo_url => <<"http://127.0.0.1:3000">>, repo_verify => false, - read_key => <<"123">>, repo_public_key => ?HEXPM_PUBLIC_KEY, repo_key => <<"repo_key">>, - username => <<"mr_pockets">>, - write_key => rebar3_hex_user:encrypt_write_key(<<"mr_pockets">>, - <<"special_shoes">>, <<"key">>), + api_key => <<"key">>, doc => #{provider => edoc} } )). @@ -32,9 +29,11 @@ J1i2xWFndWa6nfFnRxZmCStCOZWYYPlaxr+FZceFbpMwzTNs4g3d4tLNUcbKAIH4 default_config() -> ?REPO_CONFIG. mock_command(ProviderName, Command, RepoConfig, State0) -> - State1 = rebar_state:add_resource(State0, {pkg, rebar_pkg_resource}), - State2 = rebar_state:create_resources([{pkg, rebar_pkg_resource}], State1), - State3 = rebar_state:set(State2, hex, RepoConfig), + %% Set hex config BEFORE creating resources so rebar_pkg_resource:init/2 + %% picks up our test repos from rebar_hex_repos:from_state/2 + State1 = rebar_state:set(State0, hex, RepoConfig), + State2 = rebar_state:add_resource(State1, {pkg, rebar_pkg_resource}), + State3 = rebar_state:create_resources([{pkg, rebar_pkg_resource}], State2), State4 = rebar_state:command_args(State3, Command), {ok, State5} = ProviderName:init(State4),