diff --git a/rebar.config b/rebar.config index b8176ddc..7c9836a3 100644 --- a/rebar.config +++ b/rebar.config @@ -3,12 +3,13 @@ {erl_opts, [ debug_info, {platform_define, "^2[3-9]", 'POST_OTP_22'}, + {platform_define, "^23", 'OTP_23'}, {platform_define, "^20", 'POST_OTP_19'}, {platform_define, "^19", 'POST_OTP_18'}, {platform_define, "^[2-9]", 'POST_OTP_18'} ]}. -{project_plugins, [covertool, {rebar3_hank, "~> 0.3.0"}]}. +{project_plugins, [covertool, rebar3_ex_doc, {rebar3_hank, "~> 0.3.0"}]}. {deps, [{hex_core, "0.8.4"}, {verl, "1.1.1"}]}. @@ -25,8 +26,7 @@ {dialyzer, [ {warnings, [ - error_handling, - underspecs + error_handling ]}, {plt_extra_apps, [hex_core, verl]} ]}. @@ -39,3 +39,10 @@ deprecated_function_calls,deprecated_functions]}. {alias, [{test, [{ct, "--cover"}, {cover, "-v"}]}]}. + + +{ex_doc, [ + {source_url, <<"https://github.com/erlef/rebar3_hex">>}, + {extras, [<<"README.md">>, <<"LICENSE">>]}, + {main, <<"readme">>} +]}. diff --git a/src/rebar3_hex.erl b/src/rebar3_hex.erl index 8fcbf2c3..673dde0f 100644 --- a/src/rebar3_hex.erl +++ b/src/rebar3_hex.erl @@ -25,7 +25,7 @@ init(State) -> lists:foldl(fun provider_init/2, {ok, State}, [rebar3_hex_user, rebar3_hex_cut, rebar3_hex_owner, - rebar3_hex_repo, + rebar3_hex_organization, rebar3_hex_search, rebar3_hex_retire, rebar3_hex_publish]). diff --git a/src/rebar3_hex_config.erl b/src/rebar3_hex_config.erl index 889c2671..05884c1d 100644 --- a/src/rebar3_hex_config.erl +++ b/src/rebar3_hex_config.erl @@ -11,6 +11,7 @@ , hex_config_write/1 , hex_config_read/1 , repo/1 + , repo/2 , update_auth_config/2 ]). @@ -127,9 +128,11 @@ repo(State, RepoName) -> MaybeFound2 = get_repo(<>, Repos), case {MaybeFound1, MaybeFound2} of {{ok, Repo1}, undefined} -> - {ok, set_http_adapter(merge_with_env(Repo1))}; + Repo2 = set_http_adapter(merge_with_env(Repo1)), + {ok, maybe_set_api_organization(Repo2)}; {undefined, {ok, Repo2}} -> - {ok, set_http_adapter(merge_with_env(Repo2))}; + Repo3 = set_http_adapter(merge_with_env(Repo2)), + {ok, maybe_set_api_organization(Repo3)}; {undefined, undefined} -> {error, {not_valid_repo, RepoName}} end. @@ -210,7 +213,7 @@ 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(#{api_key := undefined, write_key := WriteKey, username := Username} = HexConfig) -> +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(_) -> @@ -220,3 +223,11 @@ 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; + [_,Org] -> + Repo#{api_organization => Org} + end. diff --git a/src/rebar3_hex_organization.erl b/src/rebar3_hex_organization.erl new file mode 100644 index 00000000..5be760e2 --- /dev/null +++ b/src/rebar3_hex_organization.erl @@ -0,0 +1,406 @@ +%% @doc +%% `rebar3_hex_organization' - Manage organization +%% +%% Manages the list of authorized hex organizations. +%% +%% Note that all commands that require a `NAME' argument expect a qualified repository name for the +%% argument (i.e., `hexpm:my_org'). +%% +%% == Authorize an organization == +%% +%% This command will generate an API key used to authenticate access to the organization. See the `rebar3_hex_user' +%% tasks to list and control all your active API keys. +%% +%% ``` +%% $ rebar3 hex organization auth NAME [--key KEY] [--key-name KEY_NAME] +%% ''' +%% +%% == Deauthorize and remove an organization == +%% +%% ``` +%% $ rebar3 hex organization deauth NAME +%% ''' +%% +%% == List all authorized organizations == +%% +%% This command will only list organizations you have authorized with this task, it will not list organizations you +%% have access to by having authorized with `rebar3 hex user auth'. +%% +%% == Generate organization key == +%% This command is useful to pre-generate keys for use with `rebar3 hex organization auth NAME --key KEY' on CI +%% servers or similar systems. It returns the hash of the generated key that you can pass to auth NAME --key KEY. +%% Unlike the `hex user' key commands, a key generated with this command is owned by the organization directly, +%% and not the user that generated it. This makes it ideal for shared environments such as CI where you don't +%% want to give access to user-specific resources and the user's organization membership status won't affect key. By +%% default this command sets the organization permission which allows read-only access to the organization, it can be +%% overridden with the --permission flag. +%% +%% +%% ``` +%% $ rebar3 hex organization key NAME generate [--key-name KEY_NAME] [--permission PERMISSION] +%% ''' +%% +%% == Revoke key == +%% Removes a given key from a organization. +%% +%% ``` +%% $ rebar3 hex organization key NAME revoke KEY_NAME +%% ''' +%% +%% == List keys == +%% Lists all keys associated with the organization. +%% +%% ``` +%% $ rebar3 hex organization key NAME list +%% ''' +%% +%% == Command line options == +%% +%% + +-module(rebar3_hex_organization). + +-export([ + init/1, + do/1, + format_error/1 +]). + +-include("rebar3_hex.hrl"). + +-define(PROVIDER, organization). +-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 organization auth my_org --key 1234"}, + {short_desc, "Add, remove or list configured organizations and their auth keys"}, + {desc, ""}, + {opts, [ + {all, undefined, "all", boolean, "Specifies all keys. Only recognized when used with the revoke task."}, + {key, $k, "key", string, "Authentication key for an organization that already exists at the repository."}, + {key_name, undefined, "key-name", string, "Specifies a key name to use when generating or revoking a key."}, + {permission, $p, "permission", list, "Colon delimited permission. This option may be given multiple times."} + ]} + ]), + State1 = rebar_state:add_provider(State, Provider), + {ok, State1}. + +%% @private +-spec do(rebar_state:t()) -> {ok, rebar_state:t()}. +do(State) -> + case rebar_state:command_args(State) of + ["auth", OrgName | _] -> + auth(State, to_binary(OrgName)); + ["deauth", OrgName | _] -> + deauth(State, to_binary(OrgName)); + ["key", OrgName, "generate" | _] -> + generate(State, to_binary(OrgName)); + ["key", OrgName, "revoke", "--all" | _] -> + revoke_all(State, to_binary(OrgName)); + ["key", OrgName, "revoke" | _] -> + revoke(State, to_binary(OrgName)); + ["key", OrgName, "list" | _] -> + list_org_keys(State, to_binary(OrgName)); + ["list" | _] -> + list_orgs(State); + _ -> + ?RAISE(bad_command) + end. + +%% @private +-spec format_error(any()) -> iolist(). +format_error(no_repo) -> + "Authenticate and generate commands require repository name as argument"; +format_error(auth_no_key) -> + "Repo authenticate command requires key"; + +format_error({get_write_config, {error, no_write_key}}) -> + "You are not authenticated to the parent repository as a user. " + "Authenticate with rebar3 hex user auth then run this command again."; + +format_error({auth, Reason}) when is_binary(Reason) -> + io_lib:format("Error authenticating organization : ~ts", [Reason]); +format_error({auth, Errors}) when is_map(Errors) -> + Reason = rebar3_hex_client:pretty_print_errors(Errors), + io_lib:format("Error authenticating organization : ~ts", [Reason]); + +format_error({generate_key, Reason}) when is_binary(Reason) -> + io_lib:format("Error generating organization key: ~ts", [Reason]); +format_error({generate_key, Errors}) when is_map(Errors) -> + Reason = rebar3_hex_client:pretty_print_errors(Errors), + io_lib:format("Error generating organization key: ~ts", [Reason]); + +format_error({key_generate, Reason}) when is_binary(Reason) -> + io_lib:format("Error generating organization key: ~ts", [Reason]); +format_error({key_generate, Errors}) when is_map(Errors) -> + Reason = rebar3_hex_client:pretty_print_errors(Errors), + io_lib:format("Error generating organization key: ~ts", [Reason]); + +format_error({key_revoke_all, Reason}) when is_binary(Reason) -> + io_lib:format("Error revoking all organization keys: ~ts", [Reason]); +format_error({key_revoke_all, Errors}) when is_map(Errors) -> + Reason = rebar3_hex_client:pretty_print_errors(Errors), + io_lib:format("Error revoking all organization keys: ~ts", [Reason]); + +format_error({key_list, Reason}) when is_binary(Reason) -> + io_lib:format("Error listing organization keys: ~ts", [Reason]); +format_error({key_list, Errors}) when is_map(Errors) -> + Reason = rebar3_hex_client:pretty_print_errors(Errors), + io_lib:format("Error listing organization keys: ~ts", [Reason]); + +format_error(bad_command) -> + "Invalid arguments, expected one of:\n\n" + "rebar3 hex organization auth ORG_NAME auth\n" + "rebar3 hex organization deauth ORG_NAME deauth\n" + "rebar3 hex organization key ORG_NAME generate\n" + "rebar3 hex organization key ORG_NAME revoke --key-name NAME\n" + "rebar3 hex organization key ORG_NAME revoke --all\n" + "rebar3 hex organization key ORG_NAME list\n" + "rebar3 hex organization list\n"; + +format_error(not_a_valid_repo_name) -> + "Invalid organization repository: organization name arguments must be given as a fully qualified " + "repository name (i.e, hexpm:my_org)"; + +format_error({get_repo_by_name, {error,{not_valid_repo,ParentName}}}) -> + Str = io_lib:format("You do not appear to be authenticated as a user to the ~ts repository.", [ParentName]), + Str ++ " " ++ "Run rebar3 hex user auth and try this command again."; + +format_error(Reason) -> + rebar3_hex_error:format_error(Reason). + +-dialyzer({nowarn_function, auth/2}). +-spec auth(rebar_state:t(), binary()) -> {ok, rebar_state:t()}. +auth(State, RepoName) -> + {Opts, _} = rebar_state:command_parsed_args(State), + {ParentRepo, OrgName} = get_parent_repo_and_org_name(State, RepoName), + Key = + case proplists:get_value(key, Opts, undefined) of + undefined -> + Config = get_write_config(ParentRepo), + 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)); + ProvidedKey -> + TestPerms = #{domain => <<"repository">>, resource => OrgName}, + Config = ParentRepo#{api_key => to_binary(ProvidedKey), + api_repository => OrgName, + api_organization => OrgName + }, + case rebar3_hex_client:test_key(Config, TestPerms) of + {ok, _} -> + ProvidedKey; + Error -> + ?RAISE({auth, Error}) + end + end, + rebar3_hex_config:update_auth_config(#{RepoName => #{name => RepoName, repo_key => Key}}, State), + rebar3_hex_io:say("Successfully authenticated to ~ts", [RepoName]), + {ok, State}. + +-spec deauth(rebar_state:t(), binary()) -> {ok, rebar_state:t()}. +deauth(State, RepoName) -> + ok = rebar_hex_repos:remove_from_auth_config(RepoName, State), + rebar3_hex_io:say("Successfully deauthorized ~ts", [RepoName]), + {ok, State}. + +-spec generate(rebar_state:t(), binary()) -> {ok, rebar_state:t()}. +generate(State, RepoName) -> + {Repo, 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 = get_write_config(Repo), + 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}. + +-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 = get_write_config(Repo), + case rebar3_hex_key:list(Config#{api_organization => OrgName}) of + ok -> + {ok, State}; + {error, #{<<"errors">> := Errors}} -> + ?RAISE({key_list, Errors}); + {error, #{<<"message">> := Message}} -> + ?RAISE({key_list, Message}); + Error -> + ?RAISE({key_list, Error}) + end. + +-spec revoke(rebar_state:t(), binary()) -> {ok, rebar_state:t()}. +revoke(State, RepoName) -> + {Repo, 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 -> + ?RAISE(bad_command); + K -> + K + end, + Config = get_write_config(Repo), + case rebar3_hex_key:revoke(Config#{api_organization => OrgName}, KeyName) of + ok -> + rebar3_hex_io:say("Key successfully revoked", []), + {ok, State}; + {error, #{<<"errors">> := Errors}} -> + ?RAISE({key_revoke, Errors}); + {error, #{<<"message">> := Message}} -> + ?RAISE({key_revoke, Message}); + Error -> + ?RAISE({key_revoke, Error}) + end. + +-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 = get_write_config(Repo), + case rebar3_hex_key:revoke_all(Config#{api_organization => OrgName}) of + ok -> + rebar3_hex_io:say("All keys successfully revoked", []), + {ok, State}; + {error, #{<<"errors">> := Errors}} -> + ?RAISE({key_revoke_all, Errors}); + {error, #{<<"message">> := Message}} -> + ?RAISE({key_revoke_all, Message}); + Error -> + ?RAISE({key_revoke_all, Error}) + end. + +-spec list_orgs(rebar_state:t()) -> {ok, rebar_state:t()}. +list_orgs(State) -> + Resources = rebar_state:resources(State), + #{repos := Repos} = rebar_resource_v2:find_resource_state(pkg, Resources), + Headers = ["Name", "URL", "Public Key"], + Orgs = lists:foldl( + fun(#{name := Name} = Repo, Acc) -> + case binary:split(Name, <<":">>) of + [_, _] -> + [Repo | Acc]; + _ -> + Acc + end + end, + [], + Repos + ), + + Rows = lists:map( + fun(Repo) -> + #{ + name := Name, + api_organization := Org, + repo_url := Url, + repo_public_key := PubKey + } = Repo, + [ + binary_to_list(Name), + org_url(Org, Url), + printable_public_key(PubKey) + ] + end, + Orgs + ), + rebar3_hex_results:print_table([Headers] ++ Rows), + {ok, State}. + +-spec default_perms(binary()) -> [map()]. +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), + Public = public_key:pem_entry_decode(Pem), + Hash = crypto:hash(sha256, ssh_encode(Public)), + Encoded = string:substr(base64:encode_to_string(Hash), 1, 43), + "SHA256:" ++ Encoded. + + +-ifdef(OTP_23). +-spec ssh_encode(binary()) -> binary(). +ssh_encode(InData) -> + public_key:ssh_encode(InData, ssh2_pubkey). +-elif(POST_OTP_22 and not OTP_23). +-spec ssh_encode(binary()) -> binary(). +ssh_encode(InData) -> + ssh_file:encode(InData, ssh2_pubkey). +-else. +-spec ssh_encode(binary()) -> binary(). +ssh_encode(InData) -> + public_key:ssh_encode(InData, ssh2_pubkey). +-endif. + +to_binary(Name) -> + rebar_utils:to_binary(Name). + +-spec org_url(binary(), binary()) -> [byte(), ...]. +org_url(Org, Url) -> binary_to_list(Url) ++ "/repos/" ++ binary_to_list(Org). + +-spec get_write_config(map()) -> map(). +get_write_config(Repo) -> + case rebar3_hex_config:hex_config_write(Repo) of + {ok, HexConfig} -> + HexConfig; + Error -> + ?RAISE({get_write_config, Error}) + end. + +get_parent_repo_and_org_name(State, RepoName) -> + case binary:split(RepoName, <<":">>) of + [Parent, Org] -> + case rebar3_hex_config:repo(State, Parent) of + {ok, Repo} -> + {Repo, Org}; + Error -> + ?RAISE({get_parent_repo_and_org_name, Error}) + end; + [_] -> + ?RAISE(not_a_valid_repo_name) + end. diff --git a/src/rebar3_hex_repo.erl b/src/rebar3_hex_repo.erl deleted file mode 100644 index 35fd9ad3..00000000 --- a/src/rebar3_hex_repo.erl +++ /dev/null @@ -1,143 +0,0 @@ --module(rebar3_hex_repo). - --export([init/1, - do/1, - format_error/1]). - --include("rebar3_hex.hrl"). - --define(PROVIDER, repo). --define(DEPS, []). - -%% =================================================================== -%% Public API -%% =================================================================== - --spec init(rebar_state:t()) -> {ok, rebar_state:t()}. -init(State) -> - Provider = providers:create([ - {name, ?PROVIDER}, - {module, ?MODULE}, - {namespace, hex}, - {bare, true}, - {deps, ?DEPS}, - {example, "rebar3 hex repo auth myrepo --key 1234"}, - {short_desc, "Add, remove or list configured repositories and their auth keys"}, - {desc, ""}, - {opts, [{subcmd, undefined, undefined, string, "Repo task to run"}, - {repo, undefined, undefined, string, "Name of a repository"}, - {key, $k, "key", string, "Authentication key for repository"}]} - ]), - State1 = rebar_state:add_provider(State, Provider), - {ok, State1}. - --spec do(rebar_state:t()) -> {ok, rebar_state:t()} | {error, {?MODULE, auth_no_key - | no_repo - | {bad_command, string()}}}. -do(State) -> - {Args, _} = rebar_state:command_parsed_args(State), - case proplists:get_value(subcmd, Args, undefined) of - "generate" -> - case proplists:get_value(repo, Args, undefined) of - undefined -> - ?PRV_ERROR(no_repo); - Repo -> - generate(list_to_binary(Repo), State) - end; - "auth" -> - case proplists:get_value(repo, Args, undefined) of - undefined -> - ?PRV_ERROR(no_repo); - Repo -> - case proplists:get_value(key, Args, undefined) of - undefined -> - ?PRV_ERROR(auth_no_key); - Key -> - auth(list_to_binary(Repo), list_to_binary(Key), State), - {ok, State} - end - end; - "list" -> - list_repos(State); - Command -> - ?PRV_ERROR({bad_command, Command}) - end. - --spec format_error(any()) -> iolist(). -format_error(no_repo) -> - "Authenticate and generate commands require repository name as argument"; -format_error(auth_no_key) -> - "Repo authenticate command requires key"; -format_error({bad_command, Command}) -> - io_lib:format("Unknown repo command ~ts", [Command]); -format_error(Reason) -> - rebar3_hex_error:format_error(Reason). - -auth(Repo, Key, State) -> - Config = rebar_hex_repos:auth_config(State), - RepoConfig = maps:get(Repo, Config, #{}), - RepoConfig1 = RepoConfig#{auth_key => Key}, - rebar_hex_repos:update_auth_config(#{Repo => RepoConfig1}, State). - -generate(RepoName, State) -> - {ok, RepoConfig} = rebar_hex_repos:get_repo_config(RepoName, State), - - RepoName1 = case binary:split(RepoName, <<":">>) of - [_Parent, Org] -> - Org; - Public -> - Public - end, - - Permissions = [#{<<"domain">> => <<"repository">>, - <<"resource">> => RepoName}], - Name = <>, - - {ok, HexConfig} = rebar3_hex_config:hex_config_write(RepoConfig), - case hex_api_key:add(HexConfig, Name, Permissions) of - {ok, {201, _Headers, #{<<"secret">> := Secret}}} -> - rebar3_hex_io:say("Generated key: ~ts", [Secret]), - {ok, State}; - {ok, {Status, _Headers, #{<<"message">> := Message}}} -> - ?PRV_ERROR({error, Status, Message}); - {error, Reason} -> - ?PRV_ERROR({error, Reason}) - end. - -list_repos(State) -> - Resources = rebar_state:resources(State), - #{repos := Repos} = rebar_resource_v2:find_resource_state(pkg, Resources), - Headers = ["Name", "URL", "Public Key", "Auth Key"], - Rows = lists:map(fun(Repo) -> - #{name := Name, - api_organization := Org, - repo_url := Url, - read_key := ReadKey, - repo_public_key := PubKey} = Repo, - - AuthKey = maps:get(auth_key, Repo, ReadKey), - [binary_to_list(Name), - maybe_org_url(Org, Url), - printable_public_key(PubKey), - binary_to_list(AuthKey)] - end, Repos), - rebar3_hex_results:print_table([Headers] ++ Rows), - {ok, State}. - -printable_public_key(PubKey) -> - [Pem] = public_key:pem_decode(PubKey), - Public = public_key:pem_entry_decode(Pem), - Hash = crypto:hash(sha256, ssh_encode(Public)), - Encoded = string:substr(base64:encode_to_string(Hash), 1, 43), - "SHA256:" ++ Encoded. - --ifdef(POST_OTP_22). -ssh_encode(InData) -> - ssh_file:encode(InData, ssh2_pubkey). --else. -ssh_encode(InData) -> - public_key:ssh_encode(InData, ssh2_pubkey). --endif. - -maybe_org_url(undefined, Url) -> binary_to_list(Url); -maybe_org_url(Org, Url) -> binary_to_list(Url) ++ "/repos/" ++ binary_to_list(Org). diff --git a/test/rebar3_hex_SUITE.erl b/test/rebar3_hex_SUITE.erl index 3ca1c83f..92d7c5ef 100644 --- a/test/rebar3_hex_SUITE.erl +++ b/test/rebar3_hex_SUITE.erl @@ -48,7 +48,7 @@ help_test(_Config) -> {rebar3_hex_publish, publish}, {rebar3_hex_cut, cut}, {rebar3_hex_owner, owner}, - {rebar3_hex_repo, repo}, + {rebar3_hex_organization, organization}, {rebar3_hex_retire, retire}, {rebar3_hex_search, search}, {rebar3_hex_user, user} diff --git a/test/rebar3_hex_integration_SUITE.erl b/test/rebar3_hex_integration_SUITE.erl index a003052c..304ffdfb 100644 --- a/test/rebar3_hex_integration_SUITE.erl +++ b/test/rebar3_hex_integration_SUITE.erl @@ -33,6 +33,15 @@ all() -> , whoami_error_test , whoami_unknown_test , deauth_test + , org_auth_test + , org_auth_key_test + , org_deauth_test + , org_key_generate_test + , org_key_revoke_test + , org_key_revoke_all_test + , org_key_list_test + , org_list_test + , org_bad_command_test , publish_test , publish_no_prompt_test , publish_package_with_pointless_app_arg_test @@ -117,11 +126,7 @@ data_dir(Config) -> ?config(data_dir, Config). -define(default_repo, <<"hexpm">>). --define(default_repo_config, #{repo => ?default_repo, - name => ?default_repo, - username => ?default_username, - doc => #{provider => edoc} - }). +-define(default_repo_config, test_utils:default_config()). sanity_check(_Config) -> User = <<"mr_pockets">>, @@ -222,6 +227,118 @@ auth_test(Config) -> create_user(?default_username, ?default_password, ?default_email, Repo), ?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">>}, + mocks => [] + }, + #{rebar_state := State} = Setup = setup_state(P, Config), + expect_local_password_prompt(Setup), + expects_repo_config(Setup), + expects_update_auth_config(Setup), + expects_update_auth_config_for(<<"hexpm:foo">>), + ?assertMatch({ok, State}, rebar3_hex_organization:do(State)). + +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">>}, + mocks => [] + }, + #{rebar_state := State} = Setup = setup_state(P, Config), + expect_local_password_prompt(Setup), + expects_repo_config(Setup), + expects_update_auth_config(Setup), + expects_update_auth_config_for(<<"hexpm:foo">>), + ?assertMatch({ok, State}, rebar3_hex_organization:do(State)). + +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">>}, + mocks => [] + }, + #{rebar_state := State} = Setup = setup_state(P, Config), + expects_output([{"Successfully deauthorized ~ts", [<<"hexpm:foo">>]}]), + expects_repo_config(Setup), + + expects_update_auth_config(Setup), + ?assertMatch({ok, State}, rebar3_hex_organization:do(State)). + +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">>}, + mocks => [first_auth] + }, + #{rebar_state := State} = Setup = setup_state(P, Config), + expects_repo_config(Setup), + expect_local_password_prompt(Setup), + expects_update_auth_config(Setup), + ?assertMatch({ok, State}, rebar3_hex_organization:do(State)). + +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">>}, + mocks => [key_mutation] + }, + #{rebar_state := State, repo := _Repo} = Setup = setup_state(P, Config), + expects_repo_config(Setup), + expect_local_password_prompt(Setup), + expects_update_auth_config(Setup), + ?assertMatch({ok, State}, rebar3_hex_organization:do(State)). + +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">>}, + mocks => [key_mutation] + }, + #{rebar_state := State, repo := _Repo} = Setup = setup_state(P, Config), + expects_repo_config(Setup), + expects_update_auth_config(Setup), + ?assertMatch({ok, State}, rebar3_hex_organization:do(State)). + +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">>}, + mocks => [first_auth] + }, + #{rebar_state := State} = Setup = setup_state(P, Config), + expects_repo_config(Setup), + expects_update_auth_config(Setup), + ?assertMatch({ok, State}, rebar3_hex_organization:do(State)). + +org_list_test(Config) -> + P = #{ + command => #{provider => rebar3_hex_organization, args => ["list"]}, + app => #{name => "valid"}, + repo_config => #{repo => <<"hexpm:foo">>, name => <<"hexpm:foo">>}, + mocks => [first_auth] + }, + #{rebar_state := State} = Setup = setup_state(P, Config), + expects_repo_config(Setup), + ?assertMatch({ok, State}, rebar3_hex_organization:do(State)). + +org_bad_command_test(Config) -> + P = #{ + command => #{provider => rebar3_hex_organization, args => ["key", "hexpm:foo", "eh", "--all"]}, + app => #{name => "valid"}, + mocks => [key_mutation] + }, + #{rebar_state := State, repo := _Repo} = _Setup = setup_state(P, Config), + ?assertError({error, {rebar3_hex_organization,bad_command}}, rebar3_hex_organization:do(State)). + auth_bad_local_password_test(Config) -> P = #{ command => #{provider => rebar3_hex_user, args => ["auth"]}, @@ -357,7 +474,8 @@ reset_local_password_test(Config) -> mocks => [key_mutation], username => "mr_pockets" }, - #{rebar_state := State} = setup_state(P, Config), + #{rebar_state := State} = Setup = setup_state(P, Config), + expects_update_auth_config(Setup), ?assertMatch({ok, State}, rebar3_hex_user:do(State)). reset_password_api_error_test(Config) -> @@ -929,7 +1047,7 @@ setup_state(P, Config) -> email := _Email} = Params, WriteKey = maps:get(write_key, Params, rebar3_hex_user:encrypt_write_key(Username, Password, KeyPhrase)), - DefRepoConfig = ?default_repo_config, + DefRepoConfig = test_utils:default_config(), ParamRepoConfig = maps:get(repo_config, Params, DefRepoConfig), RepoConfig = maps:merge(DefRepoConfig#{write_key => WriteKey}, ParamRepoConfig), Repo = test_utils:repo_config(RepoConfig), @@ -1189,20 +1307,34 @@ expects_registration_confirmation_output(RepoName, Email) -> expects_output([{TokenInfo, [RepoName]}, {ReqInfo, [Email]}]). expects_repo_config(#{repo := Repo}) -> - meck:expect(rebar3_hex_config, repo, fun(_) -> {ok, 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). expects_parent_repos(#{repo := Repo}) -> meck:expect(rebar3_hex_config, parent_repos, fun(_) -> {ok, [Repo]} end). expects_update_auth_config( #{username := Username, repo := Repo}) -> + BinUsername = rebar_utils:to_binary(Username), Fun = fun(Cfg, State) -> Rname = maps:get(name, Repo), Skey = maps:get(repo_key, Repo), [Rname] = maps:keys(Cfg), - #{repo_key := Skey, username := Username} = maps:get(Rname, Cfg), + #{repo_key := Skey, username := BinUsername} = maps:get(Rname, Cfg), {ok, State} end, meck:expect(rebar3_hex_config, update_auth_config, Fun). +expects_update_auth_config_for(RepoName) -> + Fun = fun(Cfg, State) -> + case Cfg of + #{RepoName := _Repo} -> + {ok, State}; + Got -> + meck:exception(error, {expected, RepoName, got, Got}) + end + end, + meck:expect(rebar3_hex_config, update_auth_config, Fun). + + %% Helper for mocking rebar3_hex_io:say/2. %% if an any atom is supplied for an argument list in the list of expected prompts %% a pattern match is performed to test equality. diff --git a/test/support/hex_api_model.erl b/test/support/hex_api_model.erl index aecd0d98..db6e9c05 100644 --- a/test/support/hex_api_model.erl +++ b/test/support/hex_api_model.erl @@ -21,6 +21,33 @@ handle(Req, _Args) -> handle(Req#req.method, elli_request:path(Req), Req). +handle('GET', [<<"auth">>], Req) -> + respond_with(200, Req, #{}); + +handle('GET', [<<"orgs">>, <<"foo">>, <<"keys">>], Req) -> + Res = [ + #{<<"name">> => <<"key1">>, + <<"inserted_at">> => <<"2019-05-27T20:49:35Z">> + }, + #{<<"name">> => <<"key2">>, + <<"inserted_at">> => <<"2019-06-27T20:49:35Z">> + } + ], + respond_with(200, Req, Res); + + +handle('POST', [<<"orgs">>, <<"foo">>, <<"keys">>], Req) -> + Res = #{ + <<"secret">> => <<"repo_key">> + }, + respond_with(201, Req, Res); + +handle('DELETE', [<<"orgs">>, <<"foo">>, <<"keys">>], Req) -> + respond_with(201, Req, #{}); + +handle('DELETE', [<<"orgs">>, <<"foo">>, <<"keys">>, <<"this-key">>], Req) -> + respond_with(201, Req, #{}); + handle('POST', [<<"packages">>, _Name, <<"releases">>, _Version, <<"docs">>], Req) -> case authenticate(Req) of {ok, #{username := _Username, email := _Email}} -> @@ -278,8 +305,11 @@ handle('DELETE', [<<"keys">>, <<"key">>], Req) -> Res = #{<<"secret">> => <<"repo_key">>}, respond_with(200, Req, Res); -handle(_Method, _Path, Req) -> - respond_with(404, Req, #{}). +handle(Method, Path, Req) -> + MethodBin = atom_to_binary(Method, utf8), + PathBin = binary:list_to_bin(lists:join(<<"/">>, Path)), + Msg = <>, + respond_with(404, Req, #{<<"message">> => Msg}). handle_event(_Event, _Data, _Args) -> ok. diff --git a/test/support/test_utils.erl b/test/support/test_utils.erl index cf22acab..fe25bada 100644 --- a/test/support/test_utils.erl +++ b/test/support/test_utils.erl @@ -1,6 +1,16 @@ -module(test_utils). --export([mkdir_p/1, make_stub/1, stub_app/1, mock_command/4, repo_config/0, repo_config/1]). +-export([default_config/0, mkdir_p/1, make_stub/1, stub_app/1, mock_command/4, repo_config/0, repo_config/1]). + +-define(HEXPM_PUBLIC_KEY, <<"-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApqREcFDt5vV21JVe2QNB +Edvzk6w36aNFhVGWN5toNJRjRJ6m4hIuG4KaXtDWVLjnvct6MYMfqhC79HAGwyF+ +IqR6Q6a5bbFSsImgBJwz1oadoVKD6ZNetAuCIK84cjMrEFRkELtEIPNHblCzUkkM +3rS9+DPlnfG8hBvGi6tvQIuZmXGCxF/73hU0/MyGhbmEjIKRtG6b0sJYKelRLTPW +XgK7s5pESgiwf2YC/2MGDXjAJfpfCd0RpLdvd4eRiXtVlE9qO9bND94E7PgQ/xqZ +J1i2xWFndWa6nfFnRxZmCStCOZWYYPlaxr+FZceFbpMwzTNs4g3d4tLNUcbKAIH4 +0wIDAQAB +-----END PUBLIC KEY-----">>). -define(REPO_CONFIG, maps:merge(hex_core:default_config(), #{ name => <<"hexpm">>, @@ -9,15 +19,18 @@ repo_url => <<"http://127.0.0.1:3000">>, repo_verify => false, read_key => <<"123">>, - repo_public_key => <<0>>, + 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">>) + <<"special_shoes">>, <<"key">>), + doc => #{provider => edoc} } )). + +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),