From d9169e2bdd9345d88e9229ec11509933a3a09aa0 Mon Sep 17 00:00:00 2001 From: ttt161 Date: Mon, 7 Aug 2023 11:46:13 +0300 Subject: [PATCH] Epic/td 651/deploy akm (#12) * add logging * debug * debug * fix auth context * fix auth context * maybe fix release * fix release deps * fix authority config * fix auth context * fix api_key entity * fix mailer * fix test * debug revoke * fix api_key entity * debug auth context * add entities to context * debug revoke * debug revoke * debug revoke * fix auth context * debug email * fix revoke * fix auth context for revoke * fix style * delete headers from revoke method * cleanup --------- Co-authored-by: anatoliy.losev --- apps/akm/priv/mails/request_revoke.dtl | 5 +++ apps/akm/src/akm_apikeys_handler.erl | 32 +++++++++++------ apps/akm/src/akm_apikeys_processing.erl | 39 +++++++++++---------- apps/akm/src/akm_auth.erl | 17 +++++++++ apps/akm/src/akm_bouncer.erl | 2 +- apps/akm/src/akm_bouncer_context.erl | 46 +++++++++++++++++-------- apps/akm/src/akm_handler.erl | 18 +++++++--- apps/akm/src/akm_mailer.erl | 28 ++++++++++++--- apps/akm/test/akm_client.erl | 6 +--- apps/akm/test/akm_cth.erl | 5 +-- elvis.config | 3 +- rebar.config | 8 +++-- rebar.lock | 6 ++-- 13 files changed, 149 insertions(+), 66 deletions(-) diff --git a/apps/akm/priv/mails/request_revoke.dtl b/apps/akm/priv/mails/request_revoke.dtl index 9759b82..9e76e63 100644 --- a/apps/akm/priv/mails/request_revoke.dtl +++ b/apps/akm/priv/mails/request_revoke.dtl @@ -1 +1,6 @@ +Subject: Revoke key +From: no-reply@empayre.com +To: You + + To revoke key, go to link: {{ url }}/apikeys/v2/orgs/{{ party_id }}/revoke-api-key/{{ api_key_id }}?apiKeyRevokeToken={{ revoke_token }} diff --git a/apps/akm/src/akm_apikeys_handler.erl b/apps/akm/src/akm_apikeys_handler.erl index 812e3d9..b5ec21c 100644 --- a/apps/akm/src/akm_apikeys_handler.erl +++ b/apps/akm/src/akm_apikeys_handler.erl @@ -78,13 +78,15 @@ prepare(OperationID = 'IssueApiKey', #{'partyId' := PartyID, 'ApiKeyIssue' := Ap end, {ok, #{authorize => Authorize, process => Process}}; prepare(OperationID = 'GetApiKey', #{'partyId' := PartyID, 'apiKeyId' := ApiKeyId}, Context, _Opts) -> + Result = akm_apikeys_processing:get_api_key(ApiKeyId), Authorize = fun() -> - Prototypes = [{operation, #{id => OperationID, party => PartyID}}], + ApiKey = extract_api_key(Result), + Prototypes = [{operation, #{id => OperationID, party => PartyID, api_key => ApiKey}}], Resolution = akm_auth:authorize_operation(Prototypes, Context), {ok, Resolution} end, Process = fun() -> - case akm_apikeys_processing:get_api_key(ApiKeyId, PartyID) of + case Result of {ok, ApiKey} -> akm_handler_utils:reply_ok(200, ApiKey); {error, not_found} -> @@ -121,8 +123,10 @@ prepare(OperationID = 'RequestRevokeApiKey', Params, Context, _Opts) -> 'apiKeyId' := ApiKeyId, 'RequestRevoke' := #{<<"status">> := Status} } = Params, + Result = akm_apikeys_processing:get_api_key(ApiKeyId), Authorize = fun() -> - Prototypes = [{operation, #{id => OperationID, party => PartyID}}], + ApiKey = extract_api_key(Result), + Prototypes = [{operation, #{id => OperationID, party => PartyID, api_key => ApiKey}}], Resolution = akm_auth:authorize_operation(Prototypes, Context), {ok, Resolution} end, @@ -137,18 +141,21 @@ prepare(OperationID = 'RequestRevokeApiKey', Params, Context, _Opts) -> end, {ok, #{authorize => Authorize, process => Process}}; prepare( - OperationID = 'RevokeApiKey', - #{'partyId' := PartyID, 'apiKeyId' := ApiKeyId, 'apiKeyRevokeToken' := Token}, - Context, + _OperationID = 'RevokeApiKey', + #{'partyId' := _PartyID, 'apiKeyId' := ApiKeyId, 'apiKeyRevokeToken' := Token}, + _Context, _Opts ) -> + %% Result = akm_apikeys_processing:get_api_key(ApiKeyId), Authorize = fun() -> - Prototypes = [{operation, #{id => OperationID, party => PartyID}}], - Resolution = akm_auth:authorize_operation(Prototypes, Context), - {ok, Resolution} + %% ApiKey = extract_api_key(Result), + %% Prototypes = [{operation, #{id => OperationID, party => PartyID, api_key => ApiKey}}], + %% Resolution = akm_auth:authorize_operation(Prototypes, Context), + %% {ok, Resolution} + {ok, allowed} end, Process = fun() -> - case akm_apikeys_processing:revoke(PartyID, ApiKeyId, Token) of + case akm_apikeys_processing:revoke(ApiKeyId, Token) of ok -> akm_handler_utils:reply_ok(204); {error, not_found} -> @@ -156,3 +163,8 @@ prepare( end end, {ok, #{authorize => Authorize, process => Process}}. + +extract_api_key({ok, Apikey}) -> + Apikey; +extract_api_key(_) -> + undefined. diff --git a/apps/akm/src/akm_apikeys_processing.erl b/apps/akm/src/akm_apikeys_processing.erl index 9db593c..5f3f7a7 100644 --- a/apps/akm/src/akm_apikeys_processing.erl +++ b/apps/akm/src/akm_apikeys_processing.erl @@ -5,10 +5,10 @@ -include_lib("epgsql/include/epgsql.hrl"). -export([issue_api_key/3]). --export([get_api_key/2]). +-export([get_api_key/1]). -export([list_api_keys/4]). -export([request_revoke/4]). --export([revoke/3]). +-export([revoke/2]). -type list_keys_response() :: #{ results => [map()], @@ -50,12 +50,12 @@ issue_api_key(PartyID, #{<<"name">> := Name} = ApiKey0, WoodyContext) -> {error, already_exists} end. --spec get_api_key(binary(), binary()) -> {ok, map()} | {error, not_found}. -get_api_key(ApiKeyId, PartyId) -> +-spec get_api_key(binary()) -> {ok, map()} | {error, not_found}. +get_api_key(ApiKeyId) -> Result = epgsql_pool:query( main_pool, - "SELECT id, name, status, metadata, created_at FROM apikeys WHERE id = $1 AND party_id = $2", - [ApiKeyId, PartyId] + "SELECT id, name, status, metadata, created_at FROM apikeys WHERE id = $1", + [ApiKeyId] ), case Result of {ok, _Columns, []} -> @@ -87,7 +87,7 @@ list_api_keys(PartyId, Status, Limit, Offset) -> -spec request_revoke(binary(), binary(), binary(), binary()) -> {ok, revoke_email_sent} | {error, not_found}. request_revoke(Email, PartyID, ApiKeyId, Status) -> - case get_full_api_key(ApiKeyId, PartyID) of + case get_full_api_key(ApiKeyId) of {error, not_found} -> {error, not_found}; {ok, _ApiKey} -> @@ -100,8 +100,8 @@ request_revoke(Email, PartyID, ApiKeyId, Status) -> epgsql_pool:query( Worker, "UPDATE apikeys SET pending_status = $1, revoke_token = $2 " - "WHERE id = $3 AND party_id = $4", - [Status, Token, ApiKeyId, PartyID] + "WHERE id = $3", + [Status, Token, ApiKeyId] ) end ) @@ -109,22 +109,23 @@ request_revoke(Email, PartyID, ApiKeyId, Status) -> {ok, 1} -> {ok, revoke_email_sent} catch - _Ex:_Er -> + Ex:Er -> + logger:error("Failed to send email with ~p:~p", [Ex, Er]), error(failed_to_send_email) end end. --spec revoke(binary(), binary(), binary()) -> ok | {error, not_found}. -revoke(PartyId, ApiKeyId, RevokeToken) -> - case get_full_api_key(ApiKeyId, PartyId) of +-spec revoke(binary(), binary()) -> ok | {error, not_found}. +revoke(ApiKeyId, RevokeToken) -> + case get_full_api_key(ApiKeyId) of {ok, #{ <<"pending_status">> := PendingStatus, <<"revoke_token">> := RevokeToken }} -> {ok, 1} = epgsql_pool:query( main_pool, - "UPDATE apikeys SET status = $1, revoke_token = null WHERE id = $2 AND party_id = $3", - [PendingStatus, ApiKeyId, PartyId] + "UPDATE apikeys SET status = $1, revoke_token = null WHERE id = $2", + [PendingStatus, ApiKeyId] ), ok; _ -> @@ -134,13 +135,13 @@ revoke(PartyId, ApiKeyId, RevokeToken) -> %% Internal functions get_authority_id() -> - application:get_env(akm, authority_id). + application:get_env(akm, authority_id, undefined). -get_full_api_key(ApiKeyId, PartyId) -> +get_full_api_key(ApiKeyId) -> Result = epgsql_pool:query( main_pool, - "SELECT * FROM apikeys WHERE id = $1 AND party_id = $2", - [ApiKeyId, PartyId] + "SELECT * FROM apikeys WHERE id = $1", + [ApiKeyId] ), case Result of {ok, _Columns, []} -> diff --git a/apps/akm/src/akm_auth.erl b/apps/akm/src/akm_auth.erl index d8463aa..9d1446f 100644 --- a/apps/akm/src/akm_auth.erl +++ b/apps/akm/src/akm_auth.erl @@ -2,6 +2,8 @@ -define(APP, akm). +-include_lib("bouncer_proto/include/bouncer_ctx_thrift.hrl"). + -export([get_subject_id/1]). -export([get_party_id/1]). -export([get_user_id/1]). @@ -12,6 +14,8 @@ -export([authorize_api_key/3]). -export([authorize_operation/2]). +-export([make_auth_context/1]). + -export_type([resolution/0]). -export_type([preauth_context/0]). -export_type([auth_context/0]). @@ -97,6 +101,19 @@ authorize_operation(Prototypes, Context) -> Fragments1 = akm_bouncer_context:build(Prototypes, Fragments), akm_bouncer:judge(Fragments1, WoodyContext). +-spec make_auth_context(binary()) -> auth_context(). +make_auth_context(PartyId) -> + { + authorized, + #{ + status => active, + context => #ctx_ContextFragment{type = 'v1_thrift_binary'}, + metadata => #{ + get_metadata_mapped_key(party_id) => PartyId + } + } + }. + %% get_token_keeper_fragment(?AUTHORIZED(#{context := Context})) -> diff --git a/apps/akm/src/akm_bouncer.erl b/apps/akm/src/akm_bouncer.erl index 78d0975..2a4632c 100644 --- a/apps/akm/src/akm_bouncer.erl +++ b/apps/akm/src/akm_bouncer.erl @@ -20,7 +20,7 @@ gather_context_fragments(TokenContextFragment, UserID, IPAddress, WoodyCtx) -> judge({Acc, External}, WoodyCtx) -> % TODO error out early? {ok, RulesetID} = application:get_env(akm, bouncer_ruleset_id), - JudgeContext = #{fragments => External#{<<"akm">> => Acc}}, + JudgeContext = #{fragments => External#{<<"apikeymgmt">> => Acc}}, bouncer_client:judge(RulesetID, JudgeContext, WoodyCtx). %% diff --git a/apps/akm/src/akm_bouncer_context.erl b/apps/akm/src/akm_bouncer_context.erl index fa17a10..4231335 100644 --- a/apps/akm/src/akm_bouncer_context.erl +++ b/apps/akm/src/akm_bouncer_context.erl @@ -19,7 +19,8 @@ -type prototype_operation() :: #{ id => operation_id(), - party => maybe_undefined(entity_id()) + party => maybe_undefined(entity_id()), + api_key => maybe_undefined(entity_id()) }. -type entity_id() :: binary(). @@ -49,28 +50,45 @@ build(Prototypes, {Acc0, External}) -> {Acc1, External}. build(operation, Params = #{id := OperationID}, Acc) -> - Acc#ctx_v1_ContextFragment{ + PartyEntity = party_entity(Params), + ApiKeyEntity = api_key_entity(Params), + ListEntities = lists:filter(fun(E) -> E =/= undefined end, [PartyEntity, ApiKeyEntity]), + Ctx = Acc#ctx_v1_ContextFragment{ apikeymgmt = #ctx_v1_ContextApiKeyMgmt{ op = #ctx_v1_ApiKeyMgmtOperation{ id = operation_id_to_binary(OperationID), - party = maybe_entity(party_id, Params), - api_key = maybe(api_key, Params) + party = PartyEntity, + api_key = ApiKeyEntity } } - }. + }, + maybe_add_entities(Ctx, ListEntities). %% -maybe(Name, Params) -> - maps:get(Name, Params, undefined). +api_key_entity( + #{ + api_key := #{ + <<"id">> := ApiKeyId, + <<"metadata">> := #{<<"party.id">> := PartyId} + } + } +) -> + #base_Entity{id = ApiKeyId, party = PartyId, type = <<"ApiKey">>}; +api_key_entity(_) -> + undefined. -maybe_entity(Name, Params) -> - case maps:get(Name, Params, undefined) of - undefined -> - undefined; - Value -> - #base_Entity{id = Value} - end. +party_entity(#{party := PartyId}) -> + #base_Entity{id = PartyId}; +party_entity(_) -> + undefined. operation_id_to_binary(V) -> erlang:atom_to_binary(V, utf8). + +maybe_add_entities(Ctx, []) -> + Ctx; +maybe_add_entities(Ctx, ListEntities) -> + Ctx#ctx_v1_ContextFragment{ + entities = ordsets:from_list(ListEntities) + }. diff --git a/apps/akm/src/akm_handler.erl b/apps/akm/src/akm_handler.erl index ffdec60..e3e35cb 100644 --- a/apps/akm/src/akm_handler.erl +++ b/apps/akm/src/akm_handler.erl @@ -60,7 +60,7 @@ authorize_api_key(OperationID, ApiKey, _Context, _HandlerOpts) -> %% request validation checks before this stage. %% But since a decent chunk of authorization logic is already defined in the handler function %% it is probably easier to move it there in its entirety. - ok = scoper:add_scope('swag.server', #{api => wallet, operation_id => OperationID}), + ok = scoper:add_scope('swag.server', #{api => apikeymgmt, operation_id => OperationID}), case akm_auth:preauthorize_api_key(ApiKey) of {ok, Context} -> {true, Context}; @@ -77,7 +77,7 @@ authorize_api_key(OperationID, ApiKey, _Context, _HandlerOpts) -> ) -> akm_apikeys_handler:request_result(). handle_request(OperationID, Req, SwagContext, Opts) -> - #{'X-Request-Deadline' := Header} = Req, + Header = maps:get('X-Request-Deadline', Req, undefined), case akm_utils:parse_deadline(Header) of {ok, Deadline} -> WoodyContext = attach_deadline(Deadline, create_woody_context(Req)), @@ -93,7 +93,7 @@ handle_request(OperationID, Req, SwagContext, Opts) -> process_request(OperationID, Req, SwagContext0, Opts, WoodyContext0) -> _ = logger:info("Processing request ~p", [OperationID]), try - SwagContext = do_authorize_api_key(SwagContext0, WoodyContext0), + SwagContext = do_authorize_api_key(OperationID, SwagContext0, WoodyContext0), WoodyContext = put_user_identity(WoodyContext0, get_auth_context(SwagContext)), Context = create_handler_context(OperationID, SwagContext, WoodyContext), ok = set_context_meta(Context), @@ -117,7 +117,9 @@ process_request(OperationID, Req, SwagContext0, Opts, WoodyContext0) -> end. -spec create_woody_context(akm_apikeys_handler:request_data()) -> woody_context:ctx(). -create_woody_context(#{'X-Request-ID' := RequestID}) -> +create_woody_context(RequestData) -> + %% use dynamic request_id if not presented + RequestID = maps:get('X-Request-ID', RequestData, new_request_id()), RpcID = #{trace_id := TraceID} = woody_context:new_rpc_id(genlib:to_binary(RequestID)), ok = scoper:add_meta(#{request_id => RequestID, trace_id => TraceID}), woody_context:new(RpcID, undefined, akm_woody_client:get_service_deadline(akm)). @@ -151,7 +153,10 @@ attach_deadline(undefined, Context) -> attach_deadline(Deadline, Context) -> woody_context:set_deadline(Deadline, Context). -do_authorize_api_key(SwagContext = #{auth_context := PreAuthContext}, WoodyContext) -> +do_authorize_api_key('RevokeApiKey', #{cowboy_req := Req} = SwagContext, _WoodyContext) -> + PartyId = cowboy_req:binding(partyId, Req), + SwagContext#{auth_context => akm_auth:make_auth_context(PartyId)}; +do_authorize_api_key(_OperationID, SwagContext = #{auth_context := PreAuthContext}, WoodyContext) -> case akm_auth:authorize_api_key(PreAuthContext, make_token_context(SwagContext), WoodyContext) of {ok, AuthContext} -> SwagContext#{auth_context => AuthContext}; @@ -188,3 +193,6 @@ process_woody_error(_Source, resource_unavailable, _Details) -> akm_handler_utils:reply_error(504); process_woody_error(_Source, result_unknown, _Details) -> akm_handler_utils:reply_error(504). + +new_request_id() -> + base64:encode(crypto:strong_rand_bytes(24)). diff --git a/apps/akm/src/akm_mailer.erl b/apps/akm/src/akm_mailer.erl index 714b7f7..5829465 100644 --- a/apps/akm/src/akm_mailer.erl +++ b/apps/akm/src/akm_mailer.erl @@ -19,12 +19,19 @@ send_revoke_mail(Email, PartyID, ApiKeyID, Token) -> {api_key_id, ApiKeyID}, {revoke_token, Token} ]), - BinaryBody = erlang:iolist_to_binary(Body), + BinaryBody = unicode:characters_to_binary(Body), + logger:info("Try send email with body: ~p", [BinaryBody]), Pid = self(), case gen_smtp_client:send( {from_email(), [Email], BinaryBody}, - [{relay, relay()}, {username, username()}, {password, password()}], + [ + {ssl, true}, + {relay, relay()}, + {port, port()}, + {username, username()}, + {password, password()} + ], fun(Result) -> erlang:send(Pid, {sending_result, Result}) end ) of @@ -54,9 +61,17 @@ password() -> #{password := Password} = get_env(), Password. +timeout() -> + maps:get(timeout, get_env(), 3000). + +port() -> + #{port := Port} = get_env(), + to_int(Port). + get_env() -> genlib_app:env(akm, mailer, #{ - url => "vality.dev", + url => "https://vality.dev", + port => 465, from_email => "example@example.com", relay => "smtp.gmail.com", username => "username", @@ -65,11 +80,16 @@ get_env() -> }). wait_result() -> + Timeout = timeout(), receive {sending_result, {ok, _Receipt}} -> ok; {sending_result, Error} -> {error, Error} - after 3000 -> + after Timeout -> {error, {failed_to_send, sending_email_timeout}} end. + +to_int(Value) when is_integer(Value) -> Value; +to_int(Value) when is_binary(Value) -> erlang:binary_to_integer(Value); +to_int(Value) when is_list(Value) -> erlang:list_to_integer(Value). diff --git a/apps/akm/test/akm_client.erl b/apps/akm/test/akm_client.erl index d2a2794..4a0cace 100644 --- a/apps/akm/test/akm_client.erl +++ b/apps/akm/test/akm_client.erl @@ -71,11 +71,7 @@ request_revoke_key(Host, Port, PartyId, ApiKeyId) -> -spec revoke_key(inet:hostname() | inet:ip_address(), inet:port_number(), binary()) -> any(). revoke_key(Host, Port, PathWithQuery) -> - Headers = [ - {<<"X-Request-ID">>, <<"revoke_key">>}, - {<<"content-type">>, <<"application/json; charset=utf-8">>}, - {<<"Authorization">>, <<"Bearer sffsdfsfsdfsdfs">>} - ], + Headers = [], ConnPid = connect(Host, Port), Answer = get(ConnPid, PathWithQuery, Headers), disconnect(ConnPid), diff --git a/apps/akm/test/akm_cth.erl b/apps/akm/test/akm_cth.erl index 7a85f6f..0033c9d 100644 --- a/apps/akm/test/akm_cth.erl +++ b/apps/akm/test/akm_cth.erl @@ -100,6 +100,7 @@ prepare_config(State) -> {mailer, #{ url => "http://vality.dev", + port => 465, from_email => "example@example.com", relay => "smtp4dev", password => "password", @@ -119,8 +120,8 @@ prepare_config(State) -> mock_services(State) -> meck:expect( - akm_auth, - authorize_operation, + akm_bouncer, + judge, fun(_, _) -> allowed end ), meck:expect( diff --git a/elvis.config b/elvis.config index df20117..f0371a5 100644 --- a/elvis.config +++ b/elvis.config @@ -23,7 +23,8 @@ {elvis_style, atom_naming_convention, #{ ignore => [ akm_apikeys_handler, - akm_apikeys_processing + akm_apikeys_processing, + akm_handler ] }}, {elvis_style, invalid_dynamic_call, #{ diff --git a/rebar.config b/rebar.config index cb77a1e..8f62fa7 100644 --- a/rebar.config +++ b/rebar.config @@ -39,8 +39,6 @@ {woody_user_identity, {git, "https://github.com/valitydev/woody_erlang_user_identity.git", {branch, "master"}}}, {bouncer_proto, {git, "https://github.com/valitydev/bouncer-proto.git", {branch, "master"}}}, {bouncer_client, {git, "https://github.com/valitydev/bouncer-client-erlang", {branch, "master"}}}, - {epgsql, {git, "https://github.com/epgsql/epgsql.git", {tag, "4.7.1"}}}, - {epgsql_pool, {git, "https://github.com/wgnet/epgsql_pool", {branch, "master"}}}, {token_keeper_client, {git, "https://github.com/valitydev/token-keeper-client", {branch, "master"}}}, %% Libraries generated with swagger-codegen-erlang from valitydev/swag-api-keys @@ -49,6 +47,11 @@ {swag_client_apikeys, {git, "https://github.com/valitydev/swag-api-keys-v2.git", {branch, "release/erlang/client/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"}}}, + %% NOTE %% Pinning to version "1.11.2" from hex here causes constant upgrading and recompilation of the entire project {jose, {git, "https://github.com/potatosalad/erlang-jose.git", {tag, "1.11.2"}}}, @@ -106,6 +109,7 @@ prometheus, prometheus_cowboy, sasl, + herd, akm ]}, {sys_config, "./config/sys.config"}, diff --git a/rebar.lock b/rebar.lock index f7cb8bd..fa1b2c9 100644 --- a/rebar.lock +++ b/rebar.lock @@ -90,7 +90,7 @@ {<<"herd">>, {git,"https://github.com/wgnet/herd.git", {ref,"934847589dcf5a6d2b02a1f546ffe91c04066f17"}}, - 1}, + 0}, {<<"identdocstore_proto">>, {git,"https://github.com/valitydev/identdocstore-proto.git", {ref,"0ab676da2bb23eb04c42e02325c40c413d74856e"}}, @@ -133,11 +133,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,"de86e82de67071276030186f4de806f1a7ff0431"}}, + {ref,"dd3ccc414fc7b08a9b62acad7aefdbc600566062"}}, 0}, {<<"swag_server_apikeys">>, {git,"https://github.com/valitydev/swag-api-keys-v2.git", - {ref,"5e27ca5e3aa6f4b44b9677e870e5c8d557fee773"}}, + {ref,"a0f3b2d46e9eba46c89b3bf81145629da8bf0a35"}}, 0}, {<<"tds_proto">>, {git,"https://github.com/valitydev/tds-proto.git",