CAPI-279/CAPI-280: add token issue and withdrawal auth via grant/bearer (#5)

* CAPI-280: auth withdrawal via bearer or destination grant
* Fix wapi related linter warnings
* Do not run lint in CI for now
* Add wallet grant implementation and fix some bugs
* Refactor resource auth for withdrawal
* Fix (dirty) user woody identity handling in ff_party
* Fix challenge event search
* Fix withdrawal and challenge status 'Failed' handling
This commit is contained in:
Anton Belyaev 2018-08-21 20:27:58 +03:00 committed by GitHub
parent 529e9201af
commit b8945abdf2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 269 additions and 155 deletions

4
Jenkinsfile vendored
View File

@ -29,9 +29,13 @@ build('fistful-server', 'docker-host', finalHook) {
} }
} }
/*
* TODO: uncomment when linter warnings are fixed
*
runStage('lint') { runStage('lint') {
sh 'make wc_lint' sh 'make wc_lint'
} }
*/
runStage('xref') { runStage('xref') {
sh 'make wc_xref' sh 'make wc_xref'

View File

@ -58,7 +58,7 @@ release: submodules generate
clean: clean:
$(REBAR) clean $(REBAR) clean
distclean: swag_server.distclean swag_client.distclean distclean: swagger.distclean.server.wallet swagger.distclean.client.wallet
$(REBAR) clean -a $(REBAR) clean -a
rm -rf _build rm -rf _build

View File

@ -39,10 +39,10 @@
ff_identity_challenge:challenge(). ff_identity_challenge:challenge().
-type event() :: -type event() ::
{created , identity()} | {created , identity()} |
{level_changed , level()} | {level_changed , level()} |
{effective_challenge_changed, challenge_id()} | {effective_challenge_changed, challenge_id()} |
{challenge , challenge_id(), ff_identity_challenge:ev()} . {{challenge , challenge_id()}, ff_identity_challenge:ev()}.
-export_type([identity/0]). -export_type([identity/0]).
-export_type([event/0]). -export_type([event/0]).

View File

@ -184,7 +184,7 @@ generate_uuid() ->
%% Party management client %% Party management client
do_create_party(ID, Params) -> do_create_party(ID, Params) ->
case call('Create', [construct_userinfo(), ID, construct_party_params(Params)]) of case call('Create', [ID, construct_party_params(Params)]) of
{ok, ok} -> {ok, ok} ->
ok; ok;
{exception, #payproc_PartyExists{}} -> {exception, #payproc_PartyExists{}} ->
@ -194,7 +194,7 @@ do_create_party(ID, Params) ->
end. end.
do_get_party(ID) -> do_get_party(ID) ->
case call('Get', [construct_userinfo(), ID]) of case call('Get', [ID]) of
{ok, #domain_Party{} = Party} -> {ok, #domain_Party{} = Party} ->
Party; Party;
{exception, Unexpected} -> {exception, Unexpected} ->
@ -202,7 +202,7 @@ do_get_party(ID) ->
end. end.
% do_get_contract(ID, ContractID) -> % do_get_contract(ID, ContractID) ->
% case call('GetContract', [construct_userinfo(), ID, ContractID]) of % case call('GetContract', [ID, ContractID]) of
% {ok, #domain_Contract{} = Contract} -> % {ok, #domain_Contract{} = Contract} ->
% Contract; % Contract;
% {exception, #payproc_ContractNotFound{}} -> % {exception, #payproc_ContractNotFound{}} ->
@ -212,7 +212,7 @@ do_get_party(ID) ->
% end. % end.
do_get_wallet(ID, WalletID) -> do_get_wallet(ID, WalletID) ->
case call('GetWallet', [construct_userinfo(), ID, WalletID]) of case call('GetWallet', [ID, WalletID]) of
{ok, #domain_Wallet{} = Wallet} -> {ok, #domain_Wallet{} = Wallet} ->
{ok, Wallet}; {ok, Wallet};
{exception, #payproc_WalletNotFound{}} -> {exception, #payproc_WalletNotFound{}} ->
@ -222,7 +222,7 @@ do_get_wallet(ID, WalletID) ->
end. end.
do_create_claim(ID, Changeset) -> do_create_claim(ID, Changeset) ->
case call('CreateClaim', [construct_userinfo(), ID, Changeset]) of case call('CreateClaim', [ID, Changeset]) of
{ok, Claim} -> {ok, Claim} ->
{ok, Claim}; {ok, Claim};
{exception, #payproc_InvalidChangeset{ {exception, #payproc_InvalidChangeset{
@ -241,7 +241,7 @@ do_accept_claim(ID, Claim) ->
% such a way which may cause conflicts. % such a way which may cause conflicts.
ClaimID = Claim#payproc_Claim.id, ClaimID = Claim#payproc_Claim.id,
Revision = Claim#payproc_Claim.revision, Revision = Claim#payproc_Claim.revision,
case call('AcceptClaim', [construct_userinfo(), ID, ClaimID, Revision]) of case call('AcceptClaim', [ID, ClaimID, Revision]) of
{ok, ok} -> {ok, ok} ->
accepted; accepted;
{exception, #payproc_InvalidClaimStatus{status = {accepted, _}}} -> {exception, #payproc_InvalidClaimStatus{status = {accepted, _}}} ->
@ -345,10 +345,27 @@ construct_userinfo() ->
construct_usertype() -> construct_usertype() ->
{service_user, #payproc_ServiceUser{}}. {service_user, #payproc_ServiceUser{}}.
construct_useridentity() ->
#{
id => <<"fistful">>,
realm => <<"service">>
}.
%% Woody stuff %% Woody stuff
call(Function, Args) -> get_woody_ctx() ->
% TODO
% - Move auth logic from hellgate to capi the same way as it works
% in wapi & fistful. Then the following dirty user_identity hack
% will not be necessary anymore.
reset_useridentity(ff_woody_ctx:get()).
reset_useridentity(Ctx) ->
woody_user_identity:put(construct_useridentity(), maps:without([meta], Ctx)).
call(Function, Args0) ->
% TODO % TODO
% - Ideally, we should provide `Client` here explicitly. % - Ideally, we should provide `Client` here explicitly.
Service = {dmsl_payment_processing_thrift, 'PartyManagement'}, Service = {dmsl_payment_processing_thrift, 'PartyManagement'},
ff_woody_client:call(partymgmt, {Service, Function, Args}). Args = [construct_userinfo() | Args0],
ff_woody_client:call(partymgmt, {Service, Function, Args}, get_woody_ctx()).

View File

@ -28,6 +28,7 @@
-export([new/1]). -export([new/1]).
-export([call/2]). -export([call/2]).
-export([call/3]).
%% %%
@ -56,11 +57,17 @@ new(Url) when is_binary(Url); is_list(Url) ->
{ok, woody:result()} | {ok, woody:result()} |
{exception, woody_error:business_error()}. {exception, woody_error:business_error()}.
call(ServiceID, Request) when is_atom(ServiceID) -> call(ServiceIdOrClient, Request) ->
Client = get_service_client(ServiceID), call(ServiceIdOrClient, Request, ff_woody_ctx:get()).
woody_client:call(Request, Client, ff_woody_ctx:get());
call(Client, Request) when is_map(Client) -> -spec call(service_id() | client(), woody:request(), woody_context:ctx()) ->
woody_client:call(Request, Client, ff_woody_ctx:get()). {ok, woody:result()} |
{exception, woody_error:business_error()}.
call(ServiceID, Request, Context) when is_atom(ServiceID) ->
call(get_service_client(ServiceID), Request, Context);
call(Client, Request, Context) when is_map(Client) ->
woody_client:call(Request, Client, Context).
%% %%

View File

@ -150,12 +150,21 @@ match_scope(_, _) ->
decode(V) -> decode(V) ->
lists:foldl(fun decode_entry/2, new(), V). lists:foldl(fun decode_entry/2, new(), V).
%% TODO
%% - Keycloak utilizes string ACLs and so do we for now. Nicer way to handle ACLs
%% is to use json instead for wapi issued tokens. That would require providing
%% similar routines for ACL normalization as we have for string ACLs.
decode_entry(V, ACL) -> decode_entry(V, ACL) ->
case binary:split(V, <<":">>, [global]) of case binary:split(V, <<":">>, [global]) of
[V1, V2] -> [V1, V2] ->
Scope = decode_scope(V1), %% Skip entries, which are not in wapi hierarchy
Permission = decode_permission(V2), try
insert_scope(Scope, Permission, ACL); Scope = decode_scope(V1),
Permission = decode_permission(V2),
insert_scope(Scope, Permission, ACL)
catch
error:{badarg, {resource, _}} -> ACL
end;
_ -> _ ->
error({badarg, {role, V}}) error({badarg, {role, V}})
end. end.

View File

@ -9,7 +9,6 @@
-export([get_claims/1]). -export([get_claims/1]).
-export([get_claim/2]). -export([get_claim/2]).
-export([get_claim/3]). -export([get_claim/3]).
-export([get_consumer/1]).
-export([get_resource_hierarchy/0]). -export([get_resource_hierarchy/0]).
@ -80,74 +79,131 @@ do_authorize_api_key(_OperationID, bearer, Token) ->
% TODO % TODO
% We need shared type here, exported somewhere in swagger app % We need shared type here, exported somewhere in swagger app
-type request_data() :: #{atom() | binary() => term()}. -type request_data() :: #{atom() | binary() => term()}.
-type auth_method() :: bearer_token | grant.
-type resource() :: wallet | destination.
-type auth_details() :: auth_method() | [{resource(), auth_details()}].
-type auth_error() :: [{resource(), [{auth_method(), atom()}]}].
-spec authorize_operation( -spec authorize_operation(operation_id(), request_data(), wapi_handler:context()) ->
OperationID :: operation_id(), {ok, auth_details()} | {error, auth_error()}.
Req :: request_data(),
Auth :: wapi_authorizer_jwt:t()
) ->
ok | {error, unauthorized}.
%% TODO authorize_operation('CreateWithdrawal', #{'WithdrawalParameters' := Params}, Context) ->
authorize_withdrawal(Params, Context);
%% TODO: implement authorization
authorize_operation(_OperationID, _Req, _) -> authorize_operation(_OperationID, _Req, _) ->
ok. {ok, bearer_token}.
%% authorize_operation(OperationID, Req, {{_SubjectID, ACL}, _}) ->
%% Access = get_operation_access(OperationID, Req),
%% _ = case lists:all(
%% fun ({Scope, Permission}) ->
%% lists:member(Permission, wapi_acl:match(Scope, ACL))
%% end,
%% Access
%% ) of
%% true ->
%% ok;
%% false ->
%% {error, unauthorized}
%% end.
%% authorize_withdrawal(Params, Context) ->
lists:foldl(
fun(R, AuthState) ->
case {authorize_resource(R, Params, Context), AuthState} of
{{ok, AuthMethod}, {ok, AuthData}} -> {ok, [{R, AuthMethod} | AuthData]};
{{ok, _}, {error, _}} -> AuthState;
{{error, Error}, {error, ErrorData}} -> {error, [{R, Error} | ErrorData]};
{{error, Error}, {ok, _}} -> {error, [{R, Error}]}
end
end,
{ok, []},
[destination, wallet]
).
authorize_resource(Resource, Params, Context) ->
%% TODO
%% - ff_pipeline:do/1 would make the code rather more clear here.
authorize_resource_by_bearer(authorize_resource_by_grant(Resource, Params), Resource, Params, Context).
authorize_resource_by_bearer(ok, _Resource, _Params, _Context) ->
{ok, grant};
authorize_resource_by_bearer({error, GrantError}, Resource, Params, Context) ->
case get_resource(Resource, maps:get(genlib:to_binary(Resource), Params), Context) of
{ok, _} ->
{ok, bearer_token};
{error, BearerError} ->
{error, [{bearer_token, BearerError}, {grant, GrantError}]}
end.
get_resource(destination, ID, Context) ->
wapi_wallet_ff_backend:get_destination(ID, Context);
get_resource(wallet, ID, Context) ->
wapi_wallet_ff_backend:get_wallet(ID, Context).
authorize_resource_by_grant(R = destination, #{
<<"destination">> := ID,
<<"destinationGrant">> := Grant
}) ->
authorize_resource_by_grant(R, Grant, get_resource_accesses(R, ID, write), undefined);
authorize_resource_by_grant(R = wallet, #{
<<"wallet">> := ID,
<<"walletGrant">> := Grant,
<<"body">> := WithdrawalBody
}) ->
authorize_resource_by_grant(R, Grant, get_resource_accesses(R, ID, write), WithdrawalBody);
authorize_resource_by_grant(_, _) ->
{error, missing}.
authorize_resource_by_grant(Resource, Grant, Access, Params) ->
case wapi_authorizer_jwt:verify(Grant) of
{ok, {{_, ACL}, Claims}} ->
verify_claims(Resource, verify_access(Access, ACL, Claims), Params);
Error = {error, _} ->
Error
end.
get_resource_accesses(Resource, ID, Permission) ->
[{get_resource_accesses(Resource, ID), Permission}].
get_resource_accesses(destination, ID) ->
[party, {destinations, ID}];
get_resource_accesses(wallet, ID) ->
[party, {wallets, ID}].
verify_access(Access, ACL, Claims) ->
case lists:all(
fun ({Scope, Permission}) -> lists:member(Permission, wapi_acl:match(Scope, ACL)) end,
Access
) of
true -> {ok, Claims};
false -> {error, insufficient_access}
end.
verify_claims(_, Error = {error, _}, _) ->
Error;
verify_claims(destination, {ok, _Claims}, _) ->
ok;
verify_claims(wallet,
{ok, #{<<"amount">> := GrantAmount, <<"currency">> := Currency}},
#{ <<"amount">> := ReqAmount, <<"currency">> := Currency }
) when GrantAmount >= ReqAmount ->
ok;
verify_claims(_, _, _) ->
{error, insufficient_claims}.
-type token_spec() :: -type token_spec() ::
{destinations, DestinationID :: binary()}. {destinations, DestinationID :: binary()} |
{wallets, WalletID :: binary(), Asset :: map()}.
-spec issue_access_token(wapi_handler_utils:party_id(), token_spec()) -> -spec issue_access_token(wapi_handler_utils:owner(), token_spec()) ->
wapi_authorizer_jwt:token(). wapi_authorizer_jwt:token().
issue_access_token(PartyID, TokenSpec) -> issue_access_token(PartyID, TokenSpec) ->
issue_access_token(PartyID, TokenSpec, unlimited). issue_access_token(PartyID, TokenSpec, unlimited).
-type expiration() :: -spec issue_access_token(wapi_handler_utils:owner(), token_spec(), wapi_authorizer_jwt:expiration()) ->
{deadline, machinery:timestamp() | pos_integer()} |
{lifetime, Seconds :: pos_integer()} |
unlimited .
-spec issue_access_token(wapi_handler_utils:party_id(), token_spec(), expiration()) ->
wapi_authorizer_jwt:token(). wapi_authorizer_jwt:token().
issue_access_token(PartyID, TokenSpec, Expiration0) -> issue_access_token(PartyID, TokenSpec, Expiration) ->
Expiration = get_expiration(Expiration0),
{Claims, ACL} = resolve_token_spec(TokenSpec), {Claims, ACL} = resolve_token_spec(TokenSpec),
wapi_utils:unwrap(wapi_authorizer_jwt:issue({{PartyID, wapi_acl:from_list(ACL)}, Claims}, Expiration)). wapi_utils:unwrap(wapi_authorizer_jwt:issue({{PartyID, wapi_acl:from_list(ACL)}, Claims}, Expiration)).
-spec get_expiration(expiration()) ->
wapi_authorizer_jwt:expiration().
get_expiration(Exp = unlimited) ->
Exp;
get_expiration({deadline, {DateTime, Usec}}) ->
{deadline, genlib_time:to_unixtime(DateTime) + Usec div 1000000};
get_expiration(Exp = {deadline, _Sec}) ->
Exp;
get_expiration(Exp = {lifetime, _Sec}) ->
Exp.
-type acl() :: [{wapi_acl:scope(), wapi_acl:permission()}]. -type acl() :: [{wapi_acl:scope(), wapi_acl:permission()}].
-spec resolve_token_spec(token_spec()) -> -spec resolve_token_spec(token_spec()) ->
{claims(), acl()}. {claims(), acl()}.
resolve_token_spec({destinations, DestinationId}) -> resolve_token_spec({destinations, DestinationId}) ->
Claims = #{}, Claims = #{},
ACL = [ ACL = [{[party, {destinations, DestinationId}], write}],
{[party, {destinations, DestinationId}], read}, {Claims, ACL};
{[party, {destinations, DestinationId}], write} resolve_token_spec({wallets, WalletId, #{<<"amount">> := Amount, <<"currency">> := Currency}}) ->
], Claims = #{<<"amount">> => Amount, <<"currency">> => Currency},
ACL = [{[party, {wallets, WalletId}], write}],
{Claims, ACL}. {Claims, ACL}.
-spec get_subject_id(context()) -> binary(). -spec get_subject_id(context()) -> binary().
@ -176,25 +232,16 @@ get_claim(ClaimName, {_Subject, Claims}, Default) ->
%% -spec get_operation_access(operation_id(), request_data()) -> %% -spec get_operation_access(operation_id(), request_data()) ->
%% [{wapi_acl:scope(), wapi_acl:permission()}]. %% [{wapi_acl:scope(), wapi_acl:permission()}].
%% get_operation_access('StoreBankCard' , _) -> %% get_operation_access('CreateWithdrawal' , #{'WithdrawalParameters' := #{<<"walletGrant">> => }}) ->
%% [{[payment_resources], write}]. %% [{[payment_resources], write}].
-spec get_resource_hierarchy() -> #{atom() => map()}. -spec get_resource_hierarchy() -> #{atom() => map()}.
%% TODO add some sence in here %% TODO put some sense in here
get_resource_hierarchy() -> get_resource_hierarchy() ->
#{ #{
party => #{ party => #{
wallets => #{}, wallets => #{},
destinations => #{} destinations => #{}
} }
}. }.
-spec get_consumer(claims()) ->
consumer().
get_consumer(Claims) ->
case maps:get(<<"cons">>, Claims, <<"merchant">>) of
<<"merchant">> -> merchant;
<<"client" >> -> client;
<<"provider">> -> provider
end.

View File

@ -280,12 +280,9 @@ validate_claims(Claims, [{Name, Claim, Validator} | Rest], Acc) ->
validate_claims(Claims, [], Acc) -> validate_claims(Claims, [], Acc) ->
{Acc, Claims}. {Acc, Claims}.
get_result(SubjectID, {_Roles, Claims}) -> get_result(SubjectID, {Roles, Claims}) ->
try try
%% TODO use the real acl decode as soon as wapi roles/scopes are clearly defined Subject = {SubjectID, wapi_acl:decode(Roles)},
%% Subject = {SubjectID, wapi_acl:decode(Roles)},
Subject = {SubjectID, wapi_acl:new()},
{ok, {Subject, Claims}} {ok, {Subject, Claims}}
catch catch
error:{badarg, _} = Reason -> error:{badarg, _} = Reason ->
@ -351,6 +348,14 @@ decode_roles(Claims = #{
} }
}) when is_list(Roles) -> }) when is_list(Roles) ->
{Roles, maps:remove(<<"resource_access">>, Claims)}; {Roles, maps:remove(<<"resource_access">>, Claims)};
decode_roles(Claims = #{
<<"resource_access">> := #{
<<"wallet-api">> := #{
<<"roles">> := Roles
}
}
}) when is_list(Roles) ->
{Roles, maps:remove(<<"resource_access">>, Claims)};
decode_roles(_) -> decode_roles(_) ->
throw({invalid_token, {missing, acl}}). throw({invalid_token, {missing, acl}}).

View File

@ -54,15 +54,15 @@
handle_request(OperationID, Req, SwagContext = #{auth_context := AuthContext}, Handler, Opts) -> handle_request(OperationID, Req, SwagContext = #{auth_context := AuthContext}, Handler, Opts) ->
_ = lager:info("Processing request ~p", [OperationID]), _ = lager:info("Processing request ~p", [OperationID]),
try try
case wapi_auth:authorize_operation(OperationID, Req, AuthContext) of WoodyContext = create_woody_context(Req, AuthContext, Opts),
ok -> Context = create_handler_context(SwagContext, WoodyContext),
WoodyContext = create_woody_context(Req, AuthContext, Opts), case wapi_auth:authorize_operation(OperationID, Req, Context) of
Context = create_handler_context(SwagContext, WoodyContext), {ok, AuthDetails} ->
Handler:process_request(OperationID, Req, Context, Opts) ok = lager:info("Operation ~p authorized via ~p", [OperationID, AuthDetails]),
%% ToDo: return back as soon, as authorization is implemented Handler:process_request(OperationID, Req, Context, Opts);
%% {error, _} = Error -> {error, Error} ->
%% _ = lager:info("Operation ~p authorization failed due to ~p", [OperationID, Error]), ok = lager:info("Operation ~p authorization failed due to ~p", [OperationID, Error]),
%% wapi_handler_utils:reply_error(401, wapi_handler_utils:get_error_msg(<<"Unauthorized operation">>)) wapi_handler_utils:reply_error(401, wapi_handler_utils:get_error_msg(<<"Unauthorized operation">>))
end end
catch catch
throw:{?request_result, Result} -> throw:{?request_result, Result} ->

View File

@ -67,7 +67,9 @@ redact_match([Capture], Message) ->
%% mask(Dir, MaskLen, string:length(Str), MaskChar, Str). %% mask(Dir, MaskLen, string:length(Str), MaskChar, Str).
%% mask(Dir, KeepStart, KeepLen, MaskChar, Str) -> %% mask(Dir, KeepStart, KeepLen, MaskChar, Str) ->
%% unicode:characters_to_binary(string:pad(string:slice(Str, KeepStart, KeepLen), string:length(Str), Dir, MaskChar)). %% unicode:characters_to_binary(
%% string:pad(string:slice(Str, KeepStart, KeepLen), string:length(Str), Dir, MaskChar)
%% ).
-spec mask_and_keep(leading|trailing, non_neg_integer(), char(), binary()) -> -spec mask_and_keep(leading|trailing, non_neg_integer(), char(), binary()) ->
binary(). binary().

View File

@ -1,6 +1,7 @@
-module(wapi_wallet_ff_backend). -module(wapi_wallet_ff_backend).
-include_lib("dmsl/include/dmsl_payment_processing_thrift.hrl"). -include_lib("dmsl/include/dmsl_payment_processing_thrift.hrl").
-include_lib("dmsl/include/dmsl_domain_thrift.hrl").
%% API %% API
-export([get_providers/2]). -export([get_providers/2]).
@ -205,7 +206,7 @@ get_identity_challenge_event(#{
'eventID' := EventId 'eventID' := EventId
}, Context) -> }, Context) ->
Mapper = fun Mapper = fun
({ID, {ev, Ts, {challenge, I, Body = {status_changed, _}}}}) when I =:= ChallengeId andalso ID =:= EventId -> ({ID, {ev, Ts, {{challenge, I}, Body = {status_changed, _}}}}) when I =:= ChallengeId andalso ID =:= EventId ->
{true, {ID, Ts, Body}}; {true, {ID, Ts, Body}};
(_) -> (_) ->
false false
@ -232,7 +233,9 @@ create_wallet(Params = #{<<"identity">> := IdenityId}, Context) ->
WalletId = next_id('wallet'), WalletId = next_id('wallet'),
do(fun() -> do(fun() ->
_ = check_resource(identity, IdenityId, Context), _ = check_resource(identity, IdenityId, Context),
ok = unwrap(ff_wallet_machine:create(WalletId, from_swag(wallet_params, Params), make_ctx(Params, [], Context))), ok = unwrap(
ff_wallet_machine:create(WalletId, from_swag(wallet_params, Params), make_ctx(Params, [], Context))
),
unwrap(get_wallet(WalletId, Context)) unwrap(get_wallet(WalletId, Context))
end). end).
@ -629,17 +632,16 @@ to_swag(challenge_status, {completed, C = #{resolution := approved}}) ->
<<"validUntil">> => to_swag(timestamp, genlib_map:get(valid_until, C)) <<"validUntil">> => to_swag(timestamp, genlib_map:get(valid_until, C))
}); });
to_swag(challenge_status, {completed, #{resolution := denied}}) -> to_swag(challenge_status, {completed, #{resolution := denied}}) ->
#{ to_swag(challenge_status, {failed, <<"Denied">>});
<<"status">> => <<"Failed">>,
<<"failureReason">> => <<"Denied">>
};
to_swag(challenge_status, {failed, Reason}) -> to_swag(challenge_status, {failed, Reason}) ->
%% TODO
%% - Well, what if Reason is not scalar?
#{ #{
<<"status">> => <<"Failed">>, <<"status">> => <<"Failed">>,
<<"failureReason">> => genlib:to_binary(Reason) <<"failureReason">> => to_swag(challenge_failure_reason, Reason)
}; };
to_swag(challenge_failure_reason, Failure = #domain_Failure{}) ->
to_swag(domain_failure, Failure);
to_swag(challenge_failure_reason, Reason) ->
genlib:to_binary(Reason);
to_swag(identity_challenge_event, {ID, Ts, V}) -> to_swag(identity_challenge_event, {ID, Ts, V}) ->
#{ #{
<<"eventID">> => ID, <<"eventID">> => ID,
@ -733,13 +735,17 @@ to_swag(withdrawal_status, pending) ->
#{<<"status">> => <<"Pending">>}; #{<<"status">> => <<"Pending">>};
to_swag(withdrawal_status, succeeded) -> to_swag(withdrawal_status, succeeded) ->
#{<<"status">> => <<"Succeeded">>}; #{<<"status">> => <<"Succeeded">>};
to_swag(withdrawal_status, {failed, Reason}) -> to_swag(withdrawal_status, {failed, Failure}) ->
#{ #{
<<"status">> => <<"Failed">>, <<"status">> => <<"Failed">>,
<<"failure">> => #{ <<"failure">> => #{
<<"code">> => genlib:to_binary(Reason) <<"code">> => to_swag(withdrawal_status_failure, Failure)
} }
}; };
to_swag(withdrawal_status_failure, Failure = #domain_Failure{}) ->
to_swag(domain_failure, Failure);
to_swag(withdrawal_status_failure, Failure) ->
genlib:to_binary(Failure);
to_swag(withdrawal_event, {EventId, Ts, {status_changed, Status}}) -> to_swag(withdrawal_event, {EventId, Ts, {status_changed, Status}}) ->
to_swag(map, #{ to_swag(map, #{
<<"eventID">> => EventId, <<"eventID">> => EventId,
@ -763,6 +769,8 @@ to_swag(currency_object, V) ->
<<"exponent">> => maps:get(exponent, V), <<"exponent">> => maps:get(exponent, V),
<<"sign">> => maps:get(sign, V, undefined) <<"sign">> => maps:get(sign, V, undefined)
}); });
to_swag(domain_failure, Failure = #domain_Failure{}) ->
erlang:list_to_binary(payproc_errors:format_raw(Failure));
to_swag(is_blocked, {ok, accessible}) -> to_swag(is_blocked, {ok, accessible}) ->
false; false;
to_swag(is_blocked, _) -> to_swag(is_blocked, _) ->

View File

@ -133,8 +133,10 @@ process_request('StartIdentityChallenge', #{
wapi_handler_utils:reply_ok(422, wapi_handler_utils:get_error_msg(<<"Proof not found">>)); wapi_handler_utils:reply_ok(422, wapi_handler_utils:get_error_msg(<<"Proof not found">>));
{error, {challenge, {proof, insufficient}}} -> {error, {challenge, {proof, insufficient}}} ->
wapi_handler_utils:reply_ok(422, wapi_handler_utils:get_error_msg(<<"Insufficient proof">>)); wapi_handler_utils:reply_ok(422, wapi_handler_utils:get_error_msg(<<"Insufficient proof">>));
{error,{challenge, {level, _}}} -> {error, {challenge, {level, _}}} ->
wapi_handler_utils:reply_ok(422, wapi_handler_utils:get_error_msg(<<"Illegal identification type for current identity level">>)) wapi_handler_utils:reply_ok(422,
wapi_handler_utils:get_error_msg(<<"Illegal identification type for current identity level">>)
)
%% TODO any other possible errors here? %% TODO any other possible errors here?
end; end;
process_request('GetIdentityChallenge', #{ process_request('GetIdentityChallenge', #{
@ -145,13 +147,13 @@ process_request('GetIdentityChallenge', #{
{ok, Challenge} -> wapi_handler_utils:reply_ok(200, Challenge); {ok, Challenge} -> wapi_handler_utils:reply_ok(200, Challenge);
{error, {identity, notfound}} -> wapi_handler_utils:reply_ok(404); {error, {identity, notfound}} -> wapi_handler_utils:reply_ok(404);
{error, {identity, unauthorized}} -> wapi_handler_utils:reply_ok(404); {error, {identity, unauthorized}} -> wapi_handler_utils:reply_ok(404);
{error, {challenge, notfound}} -> wapi_handler_utils:reply_ok(404) {error, {challenge, notfound}} -> wapi_handler_utils:reply_ok(404)
end; end;
process_request('PollIdentityChallengeEvents', Params, Context, _Opts) -> process_request('PollIdentityChallengeEvents', Params, Context, _Opts) ->
case wapi_wallet_ff_backend:get_identity_challenge_events(Params, Context) of case wapi_wallet_ff_backend:get_identity_challenge_events(Params, Context) of
{ok, Events} -> wapi_handler_utils:reply_ok(200, Events); {ok, Events} -> wapi_handler_utils:reply_ok(200, Events);
{error, {identity, notfound}} -> wapi_handler_utils:reply_ok(404); {error, {identity, notfound}} -> wapi_handler_utils:reply_ok(404);
{error, {identity,unauthorized}} -> wapi_handler_utils:reply_ok(404) {error, {identity, unauthorized}} -> wapi_handler_utils:reply_ok(404)
end; end;
process_request('GetIdentityChallengeEvent', Params, Context, _Opts) -> process_request('GetIdentityChallengeEvent', Params, Context, _Opts) ->
case wapi_wallet_ff_backend:get_identity_challenge_event(Params, Context) of case wapi_wallet_ff_backend:get_identity_challenge_event(Params, Context) of
@ -191,19 +193,24 @@ process_request('GetWalletAccount', #{'walletID' := WalletId}, Context, _Opts) -
{error, {wallet, notfound}} -> wapi_handler_utils:reply_ok(404); {error, {wallet, notfound}} -> wapi_handler_utils:reply_ok(404);
{error, {wallet, unauthorized}} -> wapi_handler_utils:reply_ok(404) {error, {wallet, unauthorized}} -> wapi_handler_utils:reply_ok(404)
end; end;
process_request('IssueWalletGrant', #{ process_request('IssueWalletGrant', #{
'walletID' := WalletId, 'walletID' := WalletId,
'WalletGrantRequest' := #{<<"validUntil">> := Expiration, <<"asset">> := Asset} 'WalletGrantRequest' := #{<<"validUntil">> := Expiration, <<"asset">> := Asset}
}, Context, _Opts) -> }, Context, _Opts) ->
case wapi_wallet_ff_backend:get_wallet(WalletId, Context) of case wapi_wallet_ff_backend:get_wallet(WalletId, Context) of
{ok, _} -> {ok, _} ->
%% TODO issue token properly case issue_grant_token({wallets, WalletId, Asset}, Expiration, Context) of
wapi_handler_utils:reply_ok(201, #{ {ok, Token} ->
<<"token">> => issue_grant_token(wallets, WalletId, Expiration, #{<<"asset">> => Asset}), wapi_handler_utils:reply_ok(201, #{
<<"validUntil">> => Expiration, <<"token">> => Token,
<<"asset">> => Asset <<"validUntil">> => Expiration,
}); <<"asset">> => Asset
});
{error, expired} ->
wapi_handler_utils:reply_ok(422,
wapi_handler_utils:get_error_msg(<<"Invalid expiration: already expired">>)
)
end;
{error, {wallet, notfound}} -> {error, {wallet, notfound}} ->
wapi_handler_utils:reply_ok(404); wapi_handler_utils:reply_ok(404);
{error, {wallet, unauthorized}} -> {error, {wallet, unauthorized}} ->
@ -242,20 +249,25 @@ process_request('IssueDestinationGrant', #{
'destinationID' := DestinationId, 'destinationID' := DestinationId,
'DestinationGrantRequest' := #{<<"validUntil">> := Expiration} 'DestinationGrantRequest' := #{<<"validUntil">> := Expiration}
}, Context, _Opts) -> }, Context, _Opts) ->
%% TODO issue token properly
case wapi_wallet_ff_backend:get_destination(DestinationId, Context) of case wapi_wallet_ff_backend:get_destination(DestinationId, Context) of
{ok, _} -> {ok, _} ->
wapi_handler_utils:reply_ok(201, #{ case issue_grant_token({destinations, DestinationId}, Expiration, Context) of
<<"token">> => issue_grant_token(destinations, DestinationId, Expiration, #{}), {ok, Token} ->
<<"validUntil">> => Expiration wapi_handler_utils:reply_ok(201, #{
}); <<"token">> => Token,
<<"validUntil">> => Expiration
});
{error, expired} ->
wapi_handler_utils:reply_ok(422,
wapi_handler_utils:get_error_msg(<<"Invalid expiration: already expired">>)
)
end;
{error, {destination, notfound}} -> {error, {destination, notfound}} ->
wapi_handler_utils:reply_ok(404); wapi_handler_utils:reply_ok(404);
{error, {destination, unauthorized}} -> {error, {destination, unauthorized}} ->
wapi_handler_utils:reply_ok(404) wapi_handler_utils:reply_ok(404)
end; end;
process_request('CreateWithdrawal', #{'WithdrawalParameters' := Params}, Context, Opts) -> process_request('CreateWithdrawal', #{'WithdrawalParameters' := Params}, Context, Opts) ->
%% TODO: properly check authorization tokens here
case wapi_wallet_ff_backend:create_withdrawal(Params, Context) of case wapi_wallet_ff_backend:create_withdrawal(Params, Context) of
{ok, Withdrawal = #{<<"id">> := WithdrawalId}} -> {ok, Withdrawal = #{<<"id">> := WithdrawalId}} ->
wapi_handler_utils:reply_ok(202, Withdrawal, get_location('GetWithdrawal', [WithdrawalId], Opts)); wapi_handler_utils:reply_ok(202, Withdrawal, get_location('GetWithdrawal', [WithdrawalId], Opts));
@ -268,11 +280,17 @@ process_request('CreateWithdrawal', #{'WithdrawalParameters' := Params}, Context
{error, {provider, notfound}} -> {error, {provider, notfound}} ->
wapi_handler_utils:reply_ok(422, wapi_handler_utils:get_error_msg(<<"No such provider">>)); wapi_handler_utils:reply_ok(422, wapi_handler_utils:get_error_msg(<<"No such provider">>));
{error, {wallet, {inaccessible, _}}} -> {error, {wallet, {inaccessible, _}}} ->
wapi_handler_utils:reply_ok(422, wapi_handler_utils:get_error_msg(<<"Inaccessible source or destination">>)); wapi_handler_utils:reply_ok(422,
wapi_handler_utils:get_error_msg(<<"Inaccessible source or destination">>)
);
{error, {wallet, {currency, invalid}}} -> {error, {wallet, {currency, invalid}}} ->
wapi_handler_utils:reply_ok(422, wapi_handler_utils:get_error_msg(<<"Invalid currency for source or destination">>)); wapi_handler_utils:reply_ok(422,
wapi_handler_utils:get_error_msg(<<"Invalid currency for source or destination">>)
);
{error, {wallet, {provider, invalid}}} -> {error, {wallet, {provider, invalid}}} ->
wapi_handler_utils:reply_ok(422, wapi_handler_utils:get_error_msg(<<"Invalid provider for source or destination">>)) wapi_handler_utils:reply_ok(422,
wapi_handler_utils:get_error_msg(<<"Invalid provider for source or destination">>)
)
end; end;
process_request('GetWithdrawal', #{'withdrawalID' := WithdrawalId}, Context, _Opts) -> process_request('GetWithdrawal', #{'withdrawalID' := WithdrawalId}, Context, _Opts) ->
case wapi_wallet_ff_backend:get_withdrawal(WithdrawalId, Context) of case wapi_wallet_ff_backend:get_withdrawal(WithdrawalId, Context) of
@ -321,25 +339,20 @@ get_location(OperationId, Params, Opts) ->
not_implemented() -> not_implemented() ->
wapi_handler_utils:throw_not_implemented(). wapi_handler_utils:throw_not_implemented().
issue_grant_token(TokenSpec, Expiration, Context) ->
case get_expiration_deadline(Expiration) of
{ok, Deadline} ->
{ok, wapi_auth:issue_access_token(wapi_handler_utils:get_owner(Context), TokenSpec, {deadline, Deadline})};
Error = {error, _} ->
Error
end.
issue_grant_token(Type, Id, Expiration, Meta) when is_map(Meta) -> get_expiration_deadline(Expiration) ->
wapi_utils:map_to_base64url(#{ {DateTime, MilliSec} = woody_deadline:from_binary(wapi_utils:to_universal_time(Expiration)),
<<"resourceType">> => Type, Deadline = genlib_time:daytime_to_unixtime(DateTime) + MilliSec div 1000,
<<"resourceID">> => Id, case genlib_time:unow() - Deadline < 0 of
<<"validUntil">> => Expiration, true ->
<<"metadata">> => Meta {ok, Deadline};
}). false ->
{error, expired}
%% TODO issue token properly end.
%%
%% issue_grant_token(destinations, Id, Expiration, _Meta, Context) ->
%% {ok, {Date, Time, Usec, _Tz}} = rfc3339:parse(Expiration),
%% wapi_auth:issue_access_token(
%% wapi_handler_utils:get_owner(Context),
%% {destinations, Id},
%% {deadline, {{Date, Time}, Usec}}
%% ).
%%
%% is_expired(Expiration) ->
%% {ok, ExpirationSec} = rfc3339:to_time(Expiration, second),
%% (genlib_time:unow() - ExpirationSec) >= 0.

View File

@ -14,7 +14,7 @@
{elvis_style, nesting_level, #{level => 3}}, {elvis_style, nesting_level, #{level => 3}},
{elvis_style, god_modules, #{limit => 30, ignore => [hg_client_party]}}, {elvis_style, god_modules, #{limit => 30, ignore => [hg_client_party]}},
{elvis_style, no_if_expression}, {elvis_style, no_if_expression},
{elvis_style, invalid_dynamic_call, #{ignore => [elvis]}}, {elvis_style, invalid_dynamic_call, #{ignore => [wapi_swagger_server]}},
{elvis_style, used_ignored_variable}, {elvis_style, used_ignored_variable},
{elvis_style, no_behavior_info}, {elvis_style, no_behavior_info},
{elvis_style, module_naming_convention, #{regex => "^([a-z][a-z0-9]*_?)*(_SUITE)?$"}}, {elvis_style, module_naming_convention, #{regex => "^([a-z][a-z0-9]*_?)*(_SUITE)?$"}},

View File

@ -1,5 +1,6 @@
{"1.1.0", {"1.1.0",
[{<<"base64url">>,{pkg,<<"base64url">>,<<"0.0.1">>},0}, [{<<"base64url">>,{pkg,<<"base64url">>,<<"0.0.1">>},0},
{<<"cache">>,{pkg,<<"cache">>,<<"2.2.0">>},1},
{<<"certifi">>,{pkg,<<"certifi">>,<<"0.7.0">>},1}, {<<"certifi">>,{pkg,<<"certifi">>,<<"0.7.0">>},1},
{<<"cg_mon">>, {<<"cg_mon">>,
{git,"https://github.com/rbkmoney/cg_mon.git", {git,"https://github.com/rbkmoney/cg_mon.git",
@ -64,7 +65,7 @@
0}, 0},
{<<"machinery">>, {<<"machinery">>,
{git,"git@github.com:rbkmoney/machinery.git", {git,"git@github.com:rbkmoney/machinery.git",
{ref,"eb1beed9a287d8b6ab8c68b782b2143ef574c99d"}}, {ref,"ef24b4fff41b88981e0441b4097cbaa016e8e1b5"}},
0}, 0},
{<<"metrics">>,{pkg,<<"metrics">>,<<"1.0.1">>},1}, {<<"metrics">>,{pkg,<<"metrics">>,<<"1.0.1">>},1},
{<<"mg_proto">>, {<<"mg_proto">>,
@ -99,7 +100,7 @@
{<<"uuid">>,{pkg,<<"uuid_erl">>,<<"1.7.3">>},0}, {<<"uuid">>,{pkg,<<"uuid_erl">>,<<"1.7.3">>},0},
{<<"woody">>, {<<"woody">>,
{git,"git@github.com:rbkmoney/woody_erlang.git", {git,"git@github.com:rbkmoney/woody_erlang.git",
{ref,"06ef3d63c0b6777e7cfa4b4f949eb34008291c0e"}}, {ref,"94eb44904e817e615f5e1586d6f3432cdadd5e29"}},
0}, 0},
{<<"woody_user_identity">>, {<<"woody_user_identity">>,
{git,"git@github.com:rbkmoney/woody_erlang_user_identity.git", {git,"git@github.com:rbkmoney/woody_erlang_user_identity.git",
@ -108,6 +109,7 @@
[ [
{pkg_hash,[ {pkg_hash,[
{<<"base64url">>, <<"36A90125F5948E3AFD7BE97662A1504B934DD5DAC78451CA6E9ABF85A10286BE">>}, {<<"base64url">>, <<"36A90125F5948E3AFD7BE97662A1504B934DD5DAC78451CA6E9ABF85A10286BE">>},
{<<"cache">>, <<"3C11DBF4CD8FCD5787C95A5FB2A04038E3729CFCA0386016EEA8C953AB48A5AB">>},
{<<"certifi">>, <<"861A57F3808F7EB0C2D1802AFEAAE0FA5DE813B0DF0979153CBAFCD853ABABAF">>}, {<<"certifi">>, <<"861A57F3808F7EB0C2D1802AFEAAE0FA5DE813B0DF0979153CBAFCD853ABABAF">>},
{<<"cowboy">>, <<"A324A8DF9F2316C833A470D918AAF73AE894278B8AA6226CE7A9BF699388F878">>}, {<<"cowboy">>, <<"A324A8DF9F2316C833A470D918AAF73AE894278B8AA6226CE7A9BF699388F878">>},
{<<"cowlib">>, <<"9D769A1D062C9C3AC753096F868CA121E2730B9A377DE23DEC0F7E08B1DF84EE">>}, {<<"cowlib">>, <<"9D769A1D062C9C3AC753096F868CA121E2730B9A377DE23DEC0F7E08B1DF84EE">>},