diff --git a/Dockerfile.dev b/Dockerfile.dev index c1ca7d0..e4cfa53 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -14,4 +14,4 @@ ENV CHARSET=UTF-8 ENV LANG=C.UTF-8 # Set runtime -CMD ["/bin/bash"] \ No newline at end of file +CMD ["/bin/bash"] diff --git a/apps/akm/src/akm.app.src b/apps/akm/src/akm.app.src index d897f93..639a366 100644 --- a/apps/akm/src/akm.app.src +++ b/apps/akm/src/akm.app.src @@ -16,8 +16,6 @@ scoper, jose, jsx, - epgsql, - epgsql_pool, cowboy_draining_server, cowboy_cors, cowboy_access_log, @@ -32,7 +30,6 @@ woody_user_identity, erlydtl, gen_smtp, - canal, opentelemetry_api, opentelemetry_exporter, opentelemetry diff --git a/apps/akm/src/akm_apikeys_handler.erl b/apps/akm/src/akm_apikeys_handler.erl index 50a2995..67e7052 100644 --- a/apps/akm/src/akm_apikeys_handler.erl +++ b/apps/akm/src/akm_apikeys_handler.erl @@ -58,7 +58,10 @@ %% Providers -spec prepare(operation_id(), request_data(), handler_context(), handler_opts()) -> {ok, request_state()}. -prepare('IssueApiKey' = OperationID, #{'partyId' := PartyID, 'ApiKeyIssue' := ApiKey}, Context, _Opts) -> +prepare(OperationID, #{'partyId' := PartyID, 'ApiKeyIssue' := ApiKey}, Context, _Opts) when + OperationID =:= 'IssueApiKey'; + OperationID =:= 'IssueApiKeyPrivate' +-> Authorize = fun() -> Prototypes = [{operation, #{id => OperationID, party => PartyID}}], Resolution = akm_auth:authorize_operation(Prototypes, Context), @@ -77,7 +80,10 @@ prepare('IssueApiKey' = OperationID, #{'partyId' := PartyID, 'ApiKeyIssue' := Ap end end, {ok, #{authorize => Authorize, process => Process}}; -prepare('GetApiKey' = OperationID, #{'partyId' := PartyID, 'apiKeyId' := ApiKeyId}, Context, _Opts) -> +prepare(OperationID, #{'partyId' := PartyID, 'apiKeyId' := ApiKeyId}, Context, _Opts) when + OperationID =:= 'GetApiKey'; + OperationID =:= 'GetApiKeyPrivate' +-> Result = akm_apikeys_processing:get_api_key(ApiKeyId), Authorize = fun() -> ApiKey = extract_api_key(Result), @@ -95,7 +101,7 @@ prepare('GetApiKey' = OperationID, #{'partyId' := PartyID, 'apiKeyId' := ApiKeyI end, {ok, #{authorize => Authorize, process => Process}}; prepare( - 'ListApiKeys' = OperationID, + OperationID, #{ 'partyId' := PartyID, 'limit' := Limit, @@ -104,7 +110,10 @@ prepare( }, Context, _Opts -) -> +) when + OperationID =:= 'ListApiKeys'; + OperationID =:= 'ListApiKeysPrivate' +-> Authorize = fun() -> Prototypes = [{operation, #{id => OperationID, party => PartyID}}], Resolution = akm_auth:authorize_operation(Prototypes, Context), @@ -139,6 +148,25 @@ prepare('RequestRevokeApiKey' = OperationID, Params, Context, _Opts) -> end end, {ok, #{authorize => Authorize, process => Process}}; +prepare('RequestRevokeApiKeyPrivate', Params, Context, _Opts) -> + #{ + 'partyId' := PartyID, + 'apiKeyId' := ApiKeyId, + 'RequestRevoke' := #{<<"status">> := Status} + } = Params, + Authorize = fun() -> + {ok, allowed} + end, + Process = fun() -> + #{woody_context := WoodyContext} = Context, + case akm_apikeys_processing:force_revoke(PartyID, ApiKeyId, Status, WoodyContext) of + ok -> + akm_handler_utils:reply_ok(204); + {error, not_found} -> + akm_handler_utils:reply_error(404) + end + end, + {ok, #{authorize => Authorize, process => Process}}; prepare( 'RevokeApiKey' = OperationID, #{'partyId' := PartyID, 'apiKeyId' := ApiKeyId, 'apiKeyRevokeToken' := Token}, diff --git a/apps/akm/src/akm_apikeys_processing.erl b/apps/akm/src/akm_apikeys_processing.erl index f9bc39f..7b71b12 100644 --- a/apps/akm/src/akm_apikeys_processing.erl +++ b/apps/akm/src/akm_apikeys_processing.erl @@ -9,6 +9,7 @@ -export([list_api_keys/4]). -export([request_revoke/4]). -export([revoke/3]). +-export([force_revoke/4]). -type list_keys_response() :: #{ results => [map()], @@ -33,7 +34,7 @@ issue_api_key(PartyID, #{<<"name">> := Name} = ApiKey0, WoodyContext) -> Client = token_keeper_client:offline_authority(get_authority_id(), WoodyContext), case token_keeper_authority_offline:create(ID, ContextFragment, Metadata, Client) of {ok, #{token := Token}} -> - {ok, _, Columns, Rows} = epgsql_pool:query( + {ok, _, Columns, Rows} = epg_pool:query( main_pool, "INSERT INTO apikeys (id, name, party_id, status, pending_status, metadata)" "VALUES ($1, $2, $3, $4, $5, $6) RETURNING id, name, status, metadata, created_at", @@ -51,7 +52,7 @@ issue_api_key(PartyID, #{<<"name">> := Name} = ApiKey0, WoodyContext) -> -spec get_api_key(binary()) -> {ok, map()} | {error, not_found}. get_api_key(ApiKeyId) -> - Result = epgsql_pool:query( + Result = epg_pool:query( main_pool, "SELECT id, name, status, metadata, created_at FROM apikeys WHERE id = $1", [ApiKeyId] @@ -87,11 +88,11 @@ request_revoke(Email, PartyID, ApiKeyId, Status) -> {ok, _ApiKey} -> Token = akm_id:generate_snowflake_id(), try - epgsql_pool:transaction( + epg_pool:transaction( main_pool, fun(Worker) -> ok = akm_mailer:send_revoke_mail(Email, PartyID, ApiKeyId, Token), - epgsql_pool:query( + epg_pool:query( Worker, "UPDATE apikeys SET pending_status = $1, revoke_token = $2 " "WHERE id = $3", @@ -109,6 +110,35 @@ request_revoke(Email, PartyID, ApiKeyId, Status) -> end end. +-spec force_revoke(binary(), binary(), binary(), binary()) -> + ok | {error, not_found}. +force_revoke(_PartyID, ApiKeyId, Status, WoodyContext) -> + case get_full_api_key(ApiKeyId) of + {error, not_found} -> + {error, not_found}; + {ok, _ApiKey} -> + Client = token_keeper_client:offline_authority(get_authority_id(), WoodyContext), + try + epg_pool:transaction( + main_pool, + fun(Worker) -> + {ok, _} = token_keeper_authority_offline:revoke(ApiKeyId, Client), + epg_pool:query( + Worker, + "UPDATE apikeys SET status = $1, revoke_token = null WHERE id = $2", + [Status, ApiKeyId] + ) + end + ) + of + {ok, 1} -> ok + catch + Ex:Er -> + logger:error("Can`t revoke ApiKey ~p with error: ~p:~p", [ApiKeyId, Ex, Er]), + {error, not_found} + end + end. + -spec revoke(binary(), binary(), woody_context()) -> ok | {error, not_found}. revoke(ApiKeyId, RevokeToken, WoodyContext) -> case get_full_api_key(ApiKeyId) of @@ -118,11 +148,11 @@ revoke(ApiKeyId, RevokeToken, WoodyContext) -> }} -> Client = token_keeper_client:offline_authority(get_authority_id(), WoodyContext), try - epgsql_pool:transaction( + epg_pool:transaction( main_pool, fun(Worker) -> {ok, _} = token_keeper_authority_offline:revoke(ApiKeyId, Client), - epgsql_pool:query( + epg_pool:query( Worker, "UPDATE apikeys SET status = $1, revoke_token = null WHERE id = $2", [PendingStatus, ApiKeyId] @@ -146,7 +176,7 @@ get_authority_id() -> application:get_env(akm, authority_id, undefined). get_full_api_key(ApiKeyId) -> - Result = epgsql_pool:query( + Result = epg_pool:query( main_pool, "SELECT * FROM apikeys WHERE id = $1", [ApiKeyId] @@ -160,14 +190,14 @@ get_full_api_key(ApiKeyId) -> end. get_keys(PartyID, undefined, Limit, Offset) -> - epgsql_pool:query( + epg_pool:query( main_pool, "SELECT id, name, status, metadata, created_at FROM apikeys where party_id = $1 " "ORDER BY created_at DESC LIMIT $2 OFFSET $3", [PartyID, Limit, Offset] ); get_keys(PartyID, Status, Limit, Offset) -> - epgsql_pool:query( + epg_pool:query( main_pool, "SELECT id, name, status, metadata, created_at FROM apikeys where party_id = $1 AND status = $2 " "ORDER BY created_at DESC LIMIT $3 OFFSET $4", diff --git a/apps/akm/src/akm_handler.erl b/apps/akm/src/akm_handler.erl index 1b859fc..b8cf74a 100644 --- a/apps/akm/src/akm_handler.erl +++ b/apps/akm/src/akm_handler.erl @@ -102,6 +102,27 @@ handle_request_(OperationID, Req, SwagContext, Opts) -> }) end. +process_request(OperationID, Req, SwagContext, Opts, WoodyContext) when + OperationID =:= 'IssueApiKeyPrivate'; + OperationID =:= 'GetApiKeyPrivate'; + OperationID =:= 'ListApiKeysPrivate'; + OperationID =:= 'RequestRevokeApiKeyPrivate' +-> + case application:get_env(akm, private_methods_enabled, false) of + true -> + _ = logger:info("Processing request ~p", [OperationID]), + try + Context = create_handler_context(OperationID, SwagContext, WoodyContext), + {ok, RequestState} = akm_apikeys_handler:prepare(OperationID, Req, Context, Opts), + #{process := Process} = RequestState, + Process() + catch + error:{woody_error, {Source, Class, Details}} -> + process_woody_error(Source, Class, Details) + end; + false -> + akm_handler_utils:reply_error(501) + end; process_request(OperationID, Req, SwagContext0, Opts, WoodyContext0) -> _ = logger:info("Processing request ~p", [OperationID]), try diff --git a/apps/akm/src/akm_sup.erl b/apps/akm/src/akm_sup.erl index 1b0f7fb..97174de 100644 --- a/apps/akm/src/akm_sup.erl +++ b/apps/akm/src/akm_sup.erl @@ -5,12 +5,6 @@ -behaviour(supervisor). --include("akm.hrl"). - --define(VAULT_TOKEN_PATH, "/var/run/secrets/kubernetes.io/serviceaccount/token"). --define(VAULT_ROLE, <<"api-key-mgmt-v2">>). --define(VAULT_KEY_PG_CREDS, <<"api-key-mgmt-v2/pg_creds">>). - %% API -export([start_link/0]). @@ -27,14 +21,13 @@ start_link() -> -spec init([]) -> {ok, {supervisor:sup_flags(), [supervisor:child_spec()]}}. init([]) -> - ok = maybe_set_secrets(), + {ok, _} = application:ensure_all_started(epg_connector), ok = dbinit(), {LogicHandlers, LogicHandlerSpecs} = get_logic_handler_info(), HealthCheck = enable_health_logging(genlib_app:env(akm, health_check, #{})), AdditionalRoutes = [{'_', [erl_health_handle:get_route(HealthCheck), get_prometheus_route()]}], SwaggerHandlerOpts = genlib_app:env(akm, swagger_handler_opts, #{}), SwaggerSpec = akm_swagger_server:child_spec(AdditionalRoutes, LogicHandlers, SwaggerHandlerOpts), - ok = start_epgsql_pooler(), {ok, { {one_for_all, 0, 1}, LogicHandlerSpecs ++ [SwaggerSpec] @@ -54,12 +47,6 @@ enable_health_logging(Check) -> EvHandler = {erl_health_event_handler, []}, maps:map(fun(_, {_, _, _} = V) -> #{runner => V, event_handler => EvHandler} end, Check). -start_epgsql_pooler() -> - Params = genlib_app:env(akm, epsql_connection, #{}), - ok = epgsql_pool:validate_connection_params(Params), - {ok, _} = epgsql_pool:start(main_pool, 10, 20, Params), - ok. - -spec get_prometheus_route() -> {iodata(), module(), _Opts :: any()}. get_prometheus_route() -> {"/metrics/[:registry]", prometheus_cowboy2_handler, []}. @@ -82,99 +69,16 @@ dbinit() -> set_database_url() -> {ok, #{ - host := PgHost, - port := PgPort, - username := PgUser, - password := PgPassword, - database := DbName - }} = application:get_env(akm, epsql_connection), - %% DATABASE_URL=postgresql://postgres:postgres@db/apikeymgmtv2 + 'api-key-mgmt-v2' := #{ + host := PgHost, + port := PgPort, + username := PgUser, + password := PgPassword, + database := DbName + } + }} = application:get_env(epg_connector, databases), + %% DATABASE_URL=postgresql://postgres:postgres@db/api-key-mgmt-v2 PgPortStr = erlang:integer_to_list(PgPort), Value = "postgresql://" ++ PgUser ++ ":" ++ PgPassword ++ "@" ++ PgHost ++ ":" ++ PgPortStr ++ "/" ++ DbName, true = os:putenv("DATABASE_URL", Value). - -maybe_set_secrets() -> - TokenPath = application:get_env(akm, vault_token_path, ?VAULT_TOKEN_PATH), - try vault_client_auth(TokenPath) of - ok -> - Key = application:get_env(akm, vault_key_pg_creds, ?VAULT_KEY_PG_CREDS), - set_secrets(canal:read(Key)); - Error -> - logger:error("can`t auth vault client with error: ~p", [Error]), - skip - catch - _:_ -> - logger:error("catch exception when auth vault client"), - skip - end, - ok. - -set_secrets( - { - ok, #{ - <<"pg_creds">> := #{ - <<"pg_user">> := PgUser, - <<"pg_password">> := PgPassword - } - } - } -) -> - logger:info("postgres credentials successfuly read from vault (as json)"), - {ok, ConnOpts} = application:get_env(akm, epsql_connection), - application:set_env( - akm, - epsql_connection, - ConnOpts#{ - username => unicode:characters_to_list(PgUser), - password => unicode:characters_to_list(PgPassword) - } - ), - ok; -set_secrets({ok, #{<<"pg_creds">> := PgCreds}}) -> - logger:info("postgres credentials successfuly read from vault (as string)"), - set_secrets({ok, #{<<"pg_creds">> => jsx:decode(PgCreds, [return_maps])}}); -set_secrets(Error) -> - logger:error("can`t read postgres credentials from vault with error: ~p", [Error]), - skip. - -vault_client_auth(TokenPath) -> - case read_maybe_linked_file(TokenPath) of - {ok, Token} -> - Role = application:get_env(akm, vault_role, ?VAULT_ROLE), - canal:auth({kubernetes, Role, Token}); - Error -> - Error - end. - -read_maybe_linked_file(MaybeLinkName) -> - case file:read_link(MaybeLinkName) of - {error, enoent} = Result -> - Result; - {error, einval} -> - file:read_file(MaybeLinkName); - {ok, Filename} -> - file:read_file(maybe_expand_relative(MaybeLinkName, Filename)) - end. - -maybe_expand_relative(BaseFilename, Filename) -> - filename:absname_join(filename:dirname(BaseFilename), Filename). - --ifdef(TEST). --include_lib("eunit/include/eunit.hrl"). - --spec test() -> _. - --spec set_secrets_error_test() -> _. -set_secrets_error_test() -> - ?assertEqual(skip, set_secrets(error)). - --spec read_error_test() -> _. -read_error_test() -> - ?assertEqual({error, enoent}, read_maybe_linked_file("unknown_file")). - --spec vault_auth_error_test() -> _. -vault_auth_error_test() -> - ?assertEqual({error, enoent}, vault_client_auth("unknown_file")). - --endif. diff --git a/apps/akm/test/akm_client.erl b/apps/akm/test/akm_client.erl index 42aa064..74d5886 100644 --- a/apps/akm/test/akm_client.erl +++ b/apps/akm/test/akm_client.erl @@ -13,6 +13,14 @@ revoke_key/3 ]). +-export([ + issue_key_private/4, + get_key_private/4, + list_keys_private/3, + list_keys_private/4, + request_revoke_key_private/4 +]). + -spec issue_key(inet:hostname() | inet:ip_address(), inet:port_number(), binary(), map()) -> any(). issue_key(Host, Port, PartyId, ApiKey) -> perform_request(Host, Port, <<"issue_key">>, fun(ConnPid, Headers) -> @@ -21,6 +29,14 @@ issue_key(Host, Port, PartyId, ApiKey) -> post(ConnPid, Path, Headers, Body) end). +-spec issue_key_private(inet:hostname() | inet:ip_address(), inet:port_number(), binary(), map()) -> any(). +issue_key_private(Host, Port, PartyId, ApiKey) -> + perform_request_private(Host, Port, <<"issue_key">>, fun(ConnPid, Headers) -> + Path = <<"/apikeys/v2/priv/", PartyId/binary, "/api-keys">>, + Body = jsx:encode(ApiKey), + post(ConnPid, Path, Headers, Body) + end). + -spec get_key(inet:hostname() | inet:ip_address(), inet:port_number(), binary(), binary()) -> any(). get_key(Host, Port, PartyId, ApiKeyId) -> perform_request(Host, Port, <<"get_key">>, fun(ConnPid, Headers) -> @@ -28,6 +44,13 @@ get_key(Host, Port, PartyId, ApiKeyId) -> get(ConnPid, Path, Headers) end). +-spec get_key_private(inet:hostname() | inet:ip_address(), inet:port_number(), binary(), binary()) -> any(). +get_key_private(Host, Port, PartyId, ApiKeyId) -> + perform_request_private(Host, Port, <<"get_key">>, fun(ConnPid, Headers) -> + Path = <<"/apikeys/v2/priv/", PartyId/binary, "/api-keys/", ApiKeyId/binary>>, + get(ConnPid, Path, Headers) + end). + -spec list_keys(inet:hostname() | inet:ip_address(), inet:port_number(), binary()) -> any(). list_keys(Host, Port, PartyId) -> list_keys(Host, Port, PartyId, [{<<"limit">>, <<"1000">>}]). @@ -40,6 +63,18 @@ list_keys(Host, Port, PartyId, QsList) -> get(ConnPid, PathWithQuery, Headers) end). +-spec list_keys_private(inet:hostname() | inet:ip_address(), inet:port_number(), binary()) -> any(). +list_keys_private(Host, Port, PartyId) -> + list_keys_private(Host, Port, PartyId, [{<<"limit">>, <<"1000">>}]). + +-spec list_keys_private(inet:hostname() | inet:ip_address(), inet:port_number(), binary(), list()) -> any(). +list_keys_private(Host, Port, PartyId, QsList) -> + perform_request_private(Host, Port, <<"list_keys">>, fun(ConnPid, Headers) -> + Path = <<"/apikeys/v2/priv/", PartyId/binary, "/api-keys">>, + PathWithQuery = maybe_query(Path, QsList), + get(ConnPid, PathWithQuery, Headers) + end). + -spec request_revoke_key(inet:hostname() | inet:ip_address(), inet:port_number(), binary(), binary()) -> any(). request_revoke_key(Host, Port, PartyId, ApiKeyId) -> perform_request(Host, Port, <<"request_revoke">>, fun(ConnPid, Headers) -> @@ -48,6 +83,14 @@ request_revoke_key(Host, Port, PartyId, ApiKeyId) -> put(ConnPid, Path, Headers, Body) end). +-spec request_revoke_key_private(inet:hostname() | inet:ip_address(), inet:port_number(), binary(), binary()) -> any(). +request_revoke_key_private(Host, Port, PartyId, ApiKeyId) -> + perform_request_private(Host, Port, <<"request_revoke">>, fun(ConnPid, Headers) -> + Path = <<"/apikeys/v2/priv/", PartyId/binary, "/api-keys/", ApiKeyId/binary, "/status">>, + Body = jsx:encode(#{<<"status">> => <<"revoked">>}), + put(ConnPid, Path, Headers, Body) + end). + -spec revoke_key(inet:hostname() | inet:ip_address(), inet:port_number(), binary()) -> any(). revoke_key(Host, Port, PathWithQuery) -> perform_request(Host, Port, <<"revoke_key">>, fun(ConnPid, Headers) -> @@ -66,6 +109,16 @@ perform_request(Host, Port, RequestID, F) -> parse(Answer) end). +perform_request_private(Host, Port, RequestID, F) -> + SpanName = iolist_to_binary(["client ", RequestID]), + ?with_span(SpanName, #{kind => ?SPAN_KIND_CLIENT}, fun(_SpanCtx) -> + Headers = prepare_headers_private(RequestID), + ConnPid = connect(Host, Port), + Answer = F(ConnPid, Headers), + disconnect(ConnPid), + parse(Answer) + end). + prepare_headers(RequestID) -> otel_propagator_text_map:inject([ {<<"X-Request-ID">>, RequestID}, @@ -73,6 +126,12 @@ prepare_headers(RequestID) -> {<<"Authorization">>, <<"Bearer sffsdfsfsdfsdfs">>} ]). +prepare_headers_private(RequestID) -> + otel_propagator_text_map:inject([ + {<<"X-Request-ID">>, RequestID}, + {<<"content-type">>, <<"application/json; charset=utf-8">>} + ]). + -spec connect(inet:hostname() | inet:ip_address(), inet:port_number()) -> any(). connect(Host, Port) -> connect(Host, Port, #{}). diff --git a/apps/akm/test/akm_ct_utils.erl b/apps/akm/test/akm_ct_utils.erl index e1d5d59..3592076 100644 --- a/apps/akm/test/akm_ct_utils.erl +++ b/apps/akm/test/akm_ct_utils.erl @@ -23,5 +23,5 @@ lookup_config(Key, Config, Default) -> -spec cleanup_db() -> ok. cleanup_db() -> - {ok, _, _} = epgsql_pool:query(main_pool, "TRUNCATE apikeys"), + {ok, _, _} = epg_pool:query(main_pool, "TRUNCATE apikeys"), ok. diff --git a/apps/akm/test/akm_cth.erl b/apps/akm/test/akm_cth.erl index 6fa3a26..873da34 100644 --- a/apps/akm/test/akm_cth.erl +++ b/apps/akm/test/akm_cth.erl @@ -86,7 +86,6 @@ prepare_config(State) -> {port, AkmPort}, {transport, thrift}, {bouncer_ruleset_id, <<"service/authz/api">>}, - {vault_token_path, WorkDir ++ "/rebar.config"}, {authority_id, <<"authority_id">>}, {health_check, #{ disk => {erl_health, disk, ["/", 99]}, @@ -94,7 +93,6 @@ prepare_config(State) -> service => {erl_health, service, [<<"api-key-mgmt-v2">>]} }}, {max_request_deadline, 60000}, - {epsql_connection, PgConfig}, {auth_config, #{ metadata_mappings => #{ party_id => <<"dev.vality.party.id">>, @@ -102,7 +100,7 @@ prepare_config(State) -> user_email => <<"dev.vality.user.email">> } }}, - + {private_methods_enabled, true}, {mailer, #{ url => "http://vality.dev", port => 465, @@ -133,9 +131,25 @@ prepare_config(State) -> }} ]}, + {epg_connector, [ + {databases, #{ + 'api-key-mgmt-v2' => PgConfig + }}, + {pools, #{ + main_pool => #{ + database => 'api-key-mgmt-v2', + size => 50 + } + }}, + {vault_token_path, WorkDir ++ "/rebar.config"}, + {vault_role, "api-key-mgmt-v2"} + ]}, + {canal, [ {url, "http://vault:8200"}, - {engine, kvv2} + {engine, kvv2}, + {httpc_options, [{ssl, [{verify, verify_none}]}]}, + {kvv2_secret_mount_path, "/secret/data/"} ]}, {opentelemetry, [ @@ -212,7 +226,7 @@ get_pg_config() -> port => list_to_integer(get_env_var("POSTGRES_PORT", "5432")), %% username => get_env_var("POSTGRES_USER", "postgres"), %% password => get_env_var("POSTGRES_PASSWORD", "postgres"), - database => get_env_var("POSTGRES_DB", "apikeymgmtv2") + database => get_env_var("POSTGRES_DB", "api-key-mgmt-v2") }. get_env_var(Name) -> diff --git a/apps/akm/test/akm_private_test_SUITE.erl b/apps/akm/test/akm_private_test_SUITE.erl new file mode 100644 index 0000000..3d01fec --- /dev/null +++ b/apps/akm/test/akm_private_test_SUITE.erl @@ -0,0 +1,244 @@ +-module(akm_private_test_SUITE). + +%% API +-export([ + init_per_suite/1, + end_per_suite/1, + init_per_testcase/2, + end_per_testcase/2, + all/0, + groups/0 +]). + +-export([issue_get_key_success_test/1]). +-export([get_unknown_key_test/1]). +-export([list_keys_test/1]). +-export([revoke_key_test/1]). +-export([list_keys_w_status_test/1]). +-export([disabled_private_api_test/1]). + +%% also defined in ct hook module akm_cth.erl +-define(ACCESS_TOKEN, <<"some.access.token">>). + +-type config() :: akm_cth:config(). +-type test_case_name() :: akm_cth:test_case_name(). +-type group_name() :: akm_cth:group_name(). +-type test_result() :: any() | no_return(). + +-spec init_per_suite(_) -> _. +init_per_suite(Config) -> + Config. + +-spec end_per_suite(_) -> _. +end_per_suite(_Config) -> + ok = akm_ct_utils:cleanup_db(), + ok. + +-spec all() -> [{group, test_case_name()}]. +all() -> + [{group, basic_operations}]. + +-spec groups() -> [{group_name(), list(), [test_case_name()]}]. +groups() -> + [ + {basic_operations, [], [ + issue_get_key_success_test, + get_unknown_key_test, + list_keys_test, + revoke_key_test, + list_keys_w_status_test, + disabled_private_api_test + ]} + ]. + +-spec init_per_testcase(test_case_name(), config()) -> config(). +init_per_testcase(revoke_key_w_email_error_test, C) -> + meck:expect( + gen_smtp_client, + send, + fun({_, _, _Msg}, _, CallbackFun) -> + P = spawn(fun() -> CallbackFun({error, {failed_to_send, sending_email_timeout}}) end), + {ok, P} + end + ), + C; +init_per_testcase(Name, C) when + Name =:= revoke_key_test; + Name =:= list_keys_w_status_test +-> + meck:expect( + gen_smtp_client, + send, + fun({_, _, Msg}, _, CallbackFun) -> + application:set_env(akm, Name, Msg), + P = spawn(fun() -> CallbackFun({ok, <<"success">>}) end), + {ok, P} + end + ), + C; +init_per_testcase(_Name, C) -> + C. + +-spec end_per_testcase(test_case_name(), config()) -> _. +end_per_testcase(Name, C) when + Name =:= revoke_key_w_email_error_test; + Name =:= revoke_key_test; + Name =:= list_keys_w_status_test +-> + meck:unload(gen_smtp_client), + C; +end_per_testcase(_Name, C) -> + C. + +-spec issue_get_key_success_test(config()) -> test_result(). +issue_get_key_success_test(Config) -> + Host = akm_ct_utils:lookup_config(akm_host, Config), + Port = akm_ct_utils:lookup_config(akm_port, Config), + ApiKeyIssue = #{ + name => <<"live-site-integration">>, + metadata => #{ + key => <<"value">> + } + }, + PartyId = <<"test_party">>, + #{ + <<"accessToken">> := ?ACCESS_TOKEN, + <<"apiKey">> := #{ + <<"createdAt">> := _DateTimeRfc3339, + <<"id">> := ApiKeyId, + <<"metadata">> := #{ + <<"key">> := <<"value">>, + <<"dev.vality.party.id">> := <<"test_party">> + }, + <<"name">> := <<"live-site-integration">>, + <<"status">> := <<"active">> + } = ExpectedApiKey + } = akm_client:issue_key_private(Host, Port, PartyId, ApiKeyIssue), + + %% check getApiKey + ExpectedApiKey = akm_client:get_key_private(Host, Port, PartyId, ApiKeyId). + +-spec get_unknown_key_test(config()) -> test_result(). +get_unknown_key_test(Config) -> + Host = akm_ct_utils:lookup_config(akm_host, Config), + Port = akm_ct_utils:lookup_config(akm_port, Config), + PartyId = <<"unknown_key_test_party">>, + not_found = akm_client:get_key_private(Host, Port, PartyId, <<"UnknownKeyId">>). + +-spec list_keys_test(config()) -> test_result(). +list_keys_test(Config) -> + Host = akm_ct_utils:lookup_config(akm_host, Config), + Port = akm_ct_utils:lookup_config(akm_port, Config), + PartyId = <<"list_test_party">>, + + %% check empty list + #{<<"results">> := []} = akm_client:list_keys_private(Host, Port, PartyId), + + ExpectedList = lists:foldl( + fun(Num, Acc) -> + #{<<"apiKey">> := ApiKey} = akm_client:issue_key_private( + Host, + Port, + PartyId, + #{name => <<(erlang:integer_to_binary(Num))/binary, "list_keys_success">>} + ), + [ApiKey | Acc] + end, + [], + lists:seq(1, 10) + ), + + %% check one batch + #{ + <<"results">> := ExpectedList + } = akm_client:list_keys_private(Host, Port, PartyId), + + %% check continuation when limit multiple of the count keys + MultLimit = <<"1">>, + ExpectedList = get_list_keys( + Host, + Port, + PartyId, + MultLimit, + akm_client:list_keys_private(Host, Port, PartyId, [{<<"limit">>, MultLimit}]), + [] + ), + + %% check continuation when limit NOT multiple of the count keys + NoMultLimit = <<"3">>, + ExpectedList = get_list_keys( + Host, + Port, + PartyId, + NoMultLimit, + akm_client:list_keys_private(Host, Port, PartyId, [{<<"limit">>, NoMultLimit}]), + [] + ). + +-spec revoke_key_test(config()) -> test_result(). +revoke_key_test(Config) -> + Host = akm_ct_utils:lookup_config(akm_host, Config), + Port = akm_ct_utils:lookup_config(akm_port, Config), + PartyId = <<"revoke_party">>, + + #{ + <<"apiKey">> := #{ + <<"id">> := ApiKeyId + } + } = akm_client:issue_key_private(Host, Port, PartyId, #{name => <<"live-site-integration">>}), + + %% check request with unknown ApiKeyId + not_found = akm_client:request_revoke_key_private(Host, Port, PartyId, <<"BadID">>), + + %% check success request revoke + {204, _, _} = akm_client:request_revoke_key_private(Host, Port, PartyId, ApiKeyId). + +-spec list_keys_w_status_test(config()) -> test_result(). +list_keys_w_status_test(Config) -> + Host = akm_ct_utils:lookup_config(akm_host, Config), + Port = akm_ct_utils:lookup_config(akm_port, Config), + PartyId = <<"list-keys-w-status-party">>, + + #{ + <<"apiKey">> := #{ + <<"id">> := RevokingApiKeyId + } = RevokingApiKey + } = akm_client:issue_key_private(Host, Port, PartyId, #{name => <<"RevokingApiKey">>}), + #{ + <<"apiKey">> := ActiveApiKey + } = akm_client:issue_key_private(Host, Port, PartyId, #{name => <<"ActiveApiKey">>}), + + {204, _, _} = akm_client:request_revoke_key_private(Host, Port, PartyId, RevokingApiKeyId), + RevokedApiKey = RevokingApiKey#{<<"status">> => <<"revoked">>}, + + %% check full list by default + #{ + <<"results">> := [ActiveApiKey, RevokedApiKey] + } = akm_client:list_keys_private(Host, Port, PartyId), + + %% check list of active keys + #{ + <<"results">> := [ActiveApiKey] + } = akm_client:list_keys_private(Host, Port, PartyId, [{<<"status">>, <<"active">>}, {<<"limit">>, <<"1000">>}]), + + %% check list of revoked keys + #{ + <<"results">> := [RevokedApiKey] + } = akm_client:list_keys_private(Host, Port, PartyId, [{<<"status">>, <<"revoked">>}, {<<"limit">>, <<"1000">>}]). + +-spec disabled_private_api_test(config()) -> test_result(). +disabled_private_api_test(Config) -> + Host = akm_ct_utils:lookup_config(akm_host, Config), + Port = akm_ct_utils:lookup_config(akm_port, Config), + _ = application:set_env(akm, private_methods_enabled, false), + {501, _, _} = akm_client:get_key_private(Host, Port, <<"PartyID">>, <<"KeyID">>). + +%% Internal functions + +get_list_keys(Host, Port, PartyId, Limit, #{<<"results">> := ListKeys, <<"continuationToken">> := Cont}, Acc) -> + Params = [{<<"limit">>, Limit}, {<<"continuationToken">>, Cont}], + get_list_keys( + Host, Port, PartyId, Limit, akm_client:list_keys_private(Host, Port, PartyId, Params), Acc ++ ListKeys + ); +get_list_keys(_Host, _Port, _PartyId, _Limit, #{<<"results">> := ListKeys}, Acc) -> + Acc ++ ListKeys. diff --git a/compose.yaml b/compose.yaml index 7b69936..33c0e6c 100644 --- a/compose.yaml +++ b/compose.yaml @@ -7,7 +7,7 @@ services: POSTGRES_HOST: db POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres - POSTGRES_DB: apikeymgmtv2 + POSTGRES_DB: api-key-mgmt-v2 build: dockerfile: Dockerfile.dev context: . @@ -27,7 +27,7 @@ services: environment: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres - POSTGRES_DB: apikeymgmtv2 + POSTGRES_DB: api-key-mgmt-v2 ports: - 5432:5432 healthcheck: diff --git a/config/sys.config b/config/sys.config index e8c4d83..aa7e179 100644 --- a/config/sys.config +++ b/config/sys.config @@ -69,14 +69,6 @@ decryption_sources => [{json, {file, <<"path/to/priv.secret">>}}] }}, - {epsql_connection, #{ - host => "db", - port => 5432, - username => "postgres", - password => "postgres", - database => "apikeymgmtv2" - }}, - {mailer, #{ url => "vality.dev", from_email => "example@example.com", @@ -86,6 +78,26 @@ }} ]}, + {epg_connector, [ + {databases, #{ + 'api-key-mgmt-v2' => #{ + host => "db", + port => 5432, + username => "postgres", + password => "postgres", + database => "api-key-mgmt-v2" + } + }}, + {pools, #{ + main_pool => #{ + database => 'api-key-mgmt-v2', + size => 50 + } + }}, + {vault_token_path, "/var/run/secrets/kubernetes.io/serviceaccount/token"}, + {vault_role, "api-key-mgmt-v2"} + ]}, + {how_are_you, [ {metrics_publishers, [ % {hay_statsd_publisher, #{ @@ -150,6 +162,8 @@ {canal, [ {url, "http://vault:8200"}, - {engine, kvv2} + {engine, kvv2}, + {httpc_options, [{ssl, [{verify, verify_none}]}]}, + {kvv2_secret_mount_path, "/secret/data/"} ]} ]. diff --git a/rebar.config b/rebar.config index 2eeae0c..8bf0552 100644 --- a/rebar.config +++ b/rebar.config @@ -52,9 +52,7 @@ {canal, {git, "https://github.com/valitydev/canal", {branch, master}}}, %% Libraries for postgres interaction - {epgsql, {git, "https://github.com/epgsql/epgsql.git", {tag, "4.7.1"}}}, - {epgsql_pool, {git, "https://github.com/wgnet/epgsql_pool", {branch, "master"}}}, - {herd, {git, "https://github.com/wgnet/herd.git", {tag, "1.3.4"}}}, + {epg_connector, {git, "https://github.com/valitydev/epg_connector.git", {branch, "master"}}}, %% NOTE %% Pinning to version "1.11.2" from hex here causes constant upgrading and recompilation of the entire project @@ -109,10 +107,11 @@ {opentelemetry, temporary}, {recon, load}, {logger_logstash_formatter, load}, + {epg_connector, load}, + {canal, load}, prometheus, prometheus_cowboy, sasl, - herd, akm ]}, {sys_config, "./config/sys.config"}, @@ -142,7 +141,9 @@ common_test, runtime_tools, meck, - gun + gun, + epgsql, + epg_connector ]} ]} ]} diff --git a/rebar.lock b/rebar.lock index afe57d8..179aa01 100644 --- a/rebar.lock +++ b/rebar.lock @@ -20,7 +20,7 @@ {<<"cache">>,{pkg,<<"cache">>,<<"2.3.3">>},1}, {<<"canal">>, {git,"https://github.com/valitydev/canal", - {ref,"621d3821cd0a6036fee75d8e3b2d17167f3268e4"}}, + {ref,"89faedce3b054bcca7cc31ca64d2ead8a9402305"}}, 0}, {<<"certifi">>,{pkg,<<"certifi">>,<<"2.6.1">>},2}, {<<"cg_mon">>, @@ -60,14 +60,14 @@ {git,"https://github.com/nuex/envloader.git", {ref,"27a97e04f35c554995467b9236d8ae0188d468c7"}}, 0}, + {<<"epg_connector">>, + {git,"https://github.com/valitydev/epg_connector.git", + {ref,"4c35b8dc26955e589323c64bd1dd0c9abe1e3c13"}}, + 0}, {<<"epgsql">>, {git,"https://github.com/epgsql/epgsql.git", {ref,"7ba52768cf0ea7d084df24d4275a88eef4db13c2"}}, - 0}, - {<<"epgsql_pool">>, - {git,"https://github.com/wgnet/epgsql_pool", - {ref,"f5e492f73752950aab932a1662536e22fc00c717"}}, - 0}, + 1}, {<<"eql">>,{pkg,<<"eql">>,<<"0.2.0">>},0}, {<<"erl_health">>, {git,"https://github.com/valitydev/erlang-health.git", @@ -96,10 +96,6 @@ {<<"grpcbox">>,{pkg,<<"grpcbox">>,<<"0.17.1">>},1}, {<<"gun">>,{pkg,<<"gun">>,<<"2.0.1">>},0}, {<<"hackney">>,{pkg,<<"hackney">>,<<"1.17.4">>},1}, - {<<"herd">>, - {git,"https://github.com/wgnet/herd.git", - {ref,"934847589dcf5a6d2b02a1f546ffe91c04066f17"}}, - 0}, {<<"hpack">>,{pkg,<<"hpack_erl">>,<<"0.3.0">>},3}, {<<"identdocstore_proto">>, {git,"https://github.com/valitydev/identdocstore-proto.git", @@ -117,7 +113,7 @@ {<<"jsone">>,{pkg,<<"jsone">>,<<"1.8.0">>},1}, {<<"jsx">>,{pkg,<<"jsx">>,<<"3.1.0">>},1}, {<<"metrics">>,{pkg,<<"metrics">>,<<"1.0.1">>},2}, - {<<"mimerl">>,{pkg,<<"mimerl">>,<<"1.3.0">>},2}, + {<<"mimerl">>,{pkg,<<"mimerl">>,<<"1.4.0">>},2}, {<<"msgpack_proto">>, {git,"https://github.com/valitydev/msgpack-proto.git", {ref,"7e447496aa5df4a5f1ace7ef2e3c31248b2a3ed0"}}, @@ -132,7 +128,6 @@ {ref,"04de2f4ad697430c75f8efa04716d30753bd7c4b"}}, 1}, {<<"parse_trans">>,{pkg,<<"parse_trans">>,<<"3.4.1">>},1}, - {<<"pooler">>,{pkg,<<"pooler">>,<<"1.5.3">>},1}, {<<"prometheus">>,{pkg,<<"prometheus">>,<<"4.11.0">>},0}, {<<"prometheus_cowboy">>,{pkg,<<"prometheus_cowboy">>,<<"0.1.9">>},0}, {<<"prometheus_httpd">>,{pkg,<<"prometheus_httpd">>,<<"2.1.14">>},1}, @@ -153,11 +148,11 @@ {<<"ssl_verify_fun">>,{pkg,<<"ssl_verify_fun">>,<<"1.1.7">>},2}, {<<"swag_client_apikeys">>, {git,"https://github.com/valitydev/swag-api-keys-v2.git", - {ref,"d05ba1eaa63b0aefff8cbdcca44227409a0a57e7"}}, + {ref,"3ced9097ecb5306079185163c926706e320cd872"}}, 0}, {<<"swag_server_apikeys">>, {git,"https://github.com/valitydev/swag-api-keys-v2.git", - {ref,"cd7d09f957b10bc24aaafd6cbbc0666553cbb417"}}, + {ref,"a6c5cadda0324c43e3a0053174ea0f8869214d77"}}, 0}, {<<"tds_proto">>, {git,"https://github.com/valitydev/tds-proto.git", @@ -178,7 +173,7 @@ {git,"https://github.com/valitydev/token-keeper-proto.git", {ref,"8b8bb4333828350301ae2fe801c0c8de61c6529c"}}, 1}, - {<<"unicode_util_compat">>,{pkg,<<"unicode_util_compat">>,<<"0.7.0">>},2}, + {<<"unicode_util_compat">>,{pkg,<<"unicode_util_compat">>,<<"0.7.1">>},2}, {<<"uuid">>, {git,"https://github.com/okeuday/uuid.git", {ref,"63e32cdad70693495163ab131456905e827a5e36"}}, @@ -215,12 +210,11 @@ {<<"jsone">>, <<"347FF1FA700E182E1F9C5012FA6D737B12C854313B9AE6954CA75D3987D6C06D">>}, {<<"jsx">>, <<"D12516BAA0BB23A59BB35DCCAF02A1BD08243FCBB9EFE24F2D9D056CCFF71268">>}, {<<"metrics">>, <<"25F094DEA2CDA98213CECC3AEFF09E940299D950904393B2A29D191C346A8486">>}, - {<<"mimerl">>, <<"D0CD9FC04B9061F82490F6581E0128379830E78535E017F7780F37FEA7545726">>}, + {<<"mimerl">>, <<"3882A5CA67FBBE7117BA8947F27643557ADEC38FA2307490C4C4207624CB213B">>}, {<<"opentelemetry">>, <<"7DDA6551EDFC3050EA4B0B40C0D2570423D6372B97E9C60793263EF62C53C3C2">>}, {<<"opentelemetry_api">>, <<"63CA1742F92F00059298F478048DFB826F4B20D49534493D6919A0DB39B6DB04">>}, {<<"opentelemetry_exporter">>, <<"5D546123230771EF4174E37BEDFD77E3374913304CD6EA3CA82A2ADD49CD5D56">>}, {<<"parse_trans">>, <<"6E6AA8167CB44CC8F39441D05193BE6E6F4E7C2946CB2759F015F8C56B76E5FF">>}, - {<<"pooler">>, <<"898CD1FA301FC42D4A8ED598CE139B71CA85B54C16AB161152B5CC5FBDCFA1A8">>}, {<<"prometheus">>, <<"B95F8DE8530F541BD95951E18E355A840003672E5EDA4788C5FA6183406BA29A">>}, {<<"prometheus_cowboy">>, <<"D9D5B300516A61ED5AE31391F8EEEEB202230081D32A1813F2D78772B6F274E1">>}, {<<"prometheus_httpd">>, <<"529A63CA2A451FC5D28C77020787A75AF661DADF721E7EC14B5842412FB67A32">>}, @@ -228,7 +222,7 @@ {<<"ranch">>, <<"25528F82BC8D7C6152C57666CA99EC716510FE0925CB188172F41CE93117B1B0">>}, {<<"ssl_verify_fun">>, <<"354C321CF377240C7B8716899E182CE4890C5938111A1296ADD3EC74CF1715DF">>}, {<<"tls_certificate_check">>, <<"2C1C7FC922A329B9EB45DDF39113C998BBDEB28A534219CD884431E2AEE1811E">>}, - {<<"unicode_util_compat">>, <<"BC84380C9AB48177092F43AC89E4DFA2C6D62B40B8BD132B1059ECC7232F9A78">>}]}, + {<<"unicode_util_compat">>, <<"A48703A25C170EEDADCA83B11E88985AF08D35F37C6F664D6DCFB106A97782FC">>}]}, {pkg_hash_ext,[ {<<"accept">>, <<"A5167FA1AE90315C3F1DD189446312F8A55D00EFA357E9C569BDA47736B874C3">>}, {<<"acceptor_pool">>, <<"0CBCD83FDC8B9AD2EEE2067EF8B91A14858A5883CB7CD800E6FCD5803E158788">>}, @@ -252,12 +246,11 @@ {<<"jsone">>, <<"08560B78624A12E0B5E7EC0271EC8CA38EF51F63D84D84843473E14D9B12618C">>}, {<<"jsx">>, <<"0C5CC8FDC11B53CC25CF65AC6705AD39E54ECC56D1C22E4ADB8F5A53FB9427F3">>}, {<<"metrics">>, <<"69B09ADDDC4F74A40716AE54D140F93BEB0FB8978D8636EADED0C31B6F099F16">>}, - {<<"mimerl">>, <<"A1E15A50D1887217DE95F0B9B0793E32853F7C258A5CD227650889B38839FE9D">>}, + {<<"mimerl">>, <<"13AF15F9F68C65884ECCA3A3891D50A7B57D82152792F3E19D88650AA126B144">>}, {<<"opentelemetry">>, <<"CDF4F51D17B592FC592B9A75F86A6F808C23044BA7CF7B9534DEBBCC5C23B0EE">>}, {<<"opentelemetry_api">>, <<"3DFBBFAA2C2ED3121C5C483162836C4F9027DEF469C41578AF5EF32589FCFC58">>}, {<<"opentelemetry_exporter">>, <<"A1F9F271F8D3B02B81462A6BFEF7075FD8457FDB06ADFF5D2537DF5E2264D9AF">>}, {<<"parse_trans">>, <<"620A406CE75DADA827B82E453C19CF06776BE266F5A67CFF34E1EF2CBB60E49A">>}, - {<<"pooler">>, <<"058D85C5081289B90E97E4DDDBC3BB5A3B4A19A728AB3BC88C689EFCC36A07C7">>}, {<<"prometheus">>, <<"719862351AABF4DF7079B05DC085D2BBCBE3AC0AC3009E956671B1D5AB88247D">>}, {<<"prometheus_cowboy">>, <<"5F71C039DEB9E9FF9DD6366BC74C907A463872B85286E619EFF0BDA15111695A">>}, {<<"prometheus_httpd">>, <<"8B39F8CB6467B80D648FB982FDEB796BAB006BB43B1C95279289F311DB562D4E">>}, @@ -265,5 +258,5 @@ {<<"ranch">>, <<"FA0B99A1780C80218A4197A59EA8D3BDAE32FBFF7E88527D7D8A4787EFF4F8E7">>}, {<<"ssl_verify_fun">>, <<"FE4C190E8F37401D30167C8C405EDA19469F34577987C76DDE613E838BBC67F8">>}, {<<"tls_certificate_check">>, <<"51A5AD3DBD72D4694848965F3B5076E8B55D70EB8D5057FCDDD536029AB8A23C">>}, - {<<"unicode_util_compat">>, <<"25EEE6D67DF61960CF6A794239566599B09E17E668D3700247BC498638152521">>}]} + {<<"unicode_util_compat">>, <<"B3A917854CE3AE233619744AD1E0102E05673136776FB2FA76234F3E03B23642">>}]} ].