diff --git a/apps/capi/src/capi.app.src b/apps/capi/src/capi.app.src index 4ce6d78..8aeca8a 100644 --- a/apps/capi/src/capi.app.src +++ b/apps/capi/src/capi.app.src @@ -15,6 +15,7 @@ woody, capi_woody_client, damsel, + fraudbusters_proto, lechiffre, bender_proto, bender_client, diff --git a/apps/capi/src/capi_handler.erl b/apps/capi/src/capi_handler.erl index 0418a0f..75530b7 100644 --- a/apps/capi/src/capi_handler.erl +++ b/apps/capi/src/capi_handler.erl @@ -101,6 +101,7 @@ map_error(validation_error, Error) -> get_handlers() -> [ + capi_handler_antifraud, capi_handler_categories, capi_handler_countries, capi_handler_invoice_templates, diff --git a/apps/capi/src/capi_handler_antifraud.erl b/apps/capi/src/capi_handler_antifraud.erl new file mode 100644 index 0000000..bfdd884 --- /dev/null +++ b/apps/capi/src/capi_handler_antifraud.erl @@ -0,0 +1,66 @@ +-module(capi_handler_antifraud). + +-include_lib("fraudbusters_proto/include/fb_proto_fraudbusters_thrift.hrl"). +-include_lib("damsel/include/dmsl_base_thrift.hrl"). + +-behaviour(capi_handler). + +-export([prepare/3]). + +-import(capi_handler_utils, [logic_error/2]). + +-spec prepare( + OperationID :: capi_handler:operation_id(), + Req :: capi_handler:request_data(), + Context :: capi_handler:processing_context() +) -> {ok, capi_handler:request_state()} | {error, noimpl}. +prepare('InspectUser' = OperationID, Req, Context) -> + Authorize = fun() -> + Prototypes = [{operation, #{id => OperationID}}], + {ok, capi_auth:authorize_operation(Prototypes, Context)} + end, + Process = fun() -> + Body = maps:get('UserInspectRequest', Req), + InspectUserContext = encode_inspect_user_context(Body), + Call = {inspector, 'InspectUserShops', {InspectUserContext}}, + case capi_handler_utils:service_call(Call, Context) of + {ok, BlockedShops} -> + {ok, {200, #{}, decode_blocked_shops(BlockedShops)}}; + {exception, #'base_InvalidRequest'{errors = Errors}} -> + FormattedErrors = capi_handler_utils:format_request_errors(Errors), + {ok, logic_error('invalidRequest', FormattedErrors)} + end + end, + {ok, #{authorize => Authorize, process => Process}}; +prepare(_OperationID, _Req, _Context) -> + {error, noimpl}. + +%% + +encode_inspect_user_context(#{<<"customer">> := Customer, <<"shops">> := Shops}) -> + #'fraudbusters_InspectUserContext'{ + user_info = encode_client_info(Customer), + shop_list = [encode_shop_context(S) || S <- Shops] + }. + +encode_client_info(Customer) -> + Device = maps:get(<<"device">>, Customer, #{}), + Contact = maps:get(<<"contact">>, Customer, #{}), + #'fraudbusters_ClientInfo'{ + ip = maps:get(<<"ip">>, Device, undefined), + fingerprint = maps:get(<<"fingerprint">>, Device, undefined), + email = maps:get(<<"email">>, Contact, undefined), + phone = maps:get(<<"phoneNumber">>, Contact, undefined) + }. + +encode_shop_context(#{<<"partyID">> := PartyID, <<"shopID">> := ShopID}) -> + #'fraudbusters_ShopContext'{ + party_id = PartyID, + shop_id = ShopID + }. + +decode_blocked_shops(#'fraudbusters_BlockedShops'{shop_list = ShopList}) -> + #{<<"blockedShops">> => [decode_shop_context(S) || S <- ShopList]}. + +decode_shop_context(#'fraudbusters_ShopContext'{party_id = PartyID, shop_id = ShopID}) -> + #{<<"partyID">> => PartyID, <<"shopID">> => ShopID}. diff --git a/apps/capi/test/capi_antifraud_tests_SUITE.erl b/apps/capi/test/capi_antifraud_tests_SUITE.erl new file mode 100644 index 0000000..05bd584 --- /dev/null +++ b/apps/capi/test/capi_antifraud_tests_SUITE.erl @@ -0,0 +1,167 @@ +-module(capi_antifraud_tests_SUITE). + +-include_lib("common_test/include/ct.hrl"). +-include_lib("stdlib/include/assert.hrl"). + +-include_lib("fraudbusters_proto/include/fb_proto_fraudbusters_thrift.hrl"). +-include_lib("capi_dummy_data.hrl"). +-include_lib("capi_bouncer_data.hrl"). + +-export([all/0]). +-export([groups/0]). +-export([init_per_suite/1]). +-export([end_per_suite/1]). +-export([init_per_group/2]). +-export([end_per_group/2]). +-export([init_per_testcase/2]). +-export([end_per_testcase/2]). + +-export([init/1]). + +-export([ + inspect_user_ok_test/1, + inspect_user_no_blocked_shops_test/1, + inspect_user_invalid_request_test/1, + inspect_user_forbidden_test/1 +]). + +-type test_case_name() :: atom(). +-type config() :: [{atom(), any()}]. +-type group_name() :: atom(). + +-behaviour(supervisor). + +-spec init([]) -> {ok, {supervisor:sup_flags(), [supervisor:child_spec()]}}. +init([]) -> + {ok, {#{strategy => one_for_all, intensity => 1, period => 1}, []}}. + +-spec all() -> [{group, test_case_name()}]. +all() -> + [{group, operations_by_user_session_token}]. + +-spec groups() -> [{group_name(), list(), [test_case_name()]}]. +groups() -> + [ + {operations_by_user_session_token, [], [ + inspect_user_ok_test, + inspect_user_no_blocked_shops_test, + inspect_user_invalid_request_test, + inspect_user_forbidden_test + ]} + ]. + +-spec init_per_suite(config()) -> config(). +init_per_suite(Config) -> + capi_ct_helper:init_suite(?MODULE, Config). + +-spec end_per_suite(config()) -> _. +end_per_suite(C) -> + _ = capi_ct_helper:stop_mocked_service_sup(?config(suite_test_sup, C)), + _ = [application:stop(App) || App <- proplists:get_value(apps, C)], + ok. + +-spec init_per_group(group_name(), config()) -> config(). +init_per_group(operations_by_user_session_token, Config) -> + SupPid = capi_ct_helper:start_mocked_service_sup(?MODULE), + Apps = capi_ct_helper_token_keeper:mock_user_session_token(SupPid), + [{context, capi_ct_helper:get_context(?API_TOKEN)}, {group_apps, Apps}, {group_test_sup, SupPid} | Config]. + +-spec end_per_group(group_name(), config()) -> _. +end_per_group(operations_by_user_session_token, C) -> + capi_utils:'maybe'(?config(group_test_sup, C), fun capi_ct_helper:stop_mocked_service_sup/1); +end_per_group(_Group, _C) -> + ok. + +-spec init_per_testcase(test_case_name(), config()) -> config(). +init_per_testcase(_Name, C) -> + [{test_sup, capi_ct_helper:start_mocked_service_sup(?MODULE)} | C]. + +-spec end_per_testcase(test_case_name(), config()) -> _. +end_per_testcase(_Name, C) -> + capi_ct_helper:stop_mocked_service_sup(?config(test_sup, C)), + ok. + +%%% Tests + +-define(PARTY_ID, <<"party-1">>). +-define(SHOP_ID_1, <<"shop-1">>). +-define(SHOP_ID_2, <<"shop-2">>). + +-define(INSPECT_USER_REQUEST, #{ + <<"customer">> => #{ + <<"device">> => #{ + <<"fingerprint">> => <<"abc123">>, + <<"ip">> => <<"192.168.1.1">> + }, + <<"contact">> => #{ + <<"email">> => <<"test@example.com">>, + <<"phoneNumber">> => <<"+79001234567">> + } + }, + <<"shops">> => [ + #{<<"partyID">> => ?PARTY_ID, <<"shopID">> => ?SHOP_ID_1}, + #{<<"partyID">> => ?PARTY_ID, <<"shopID">> => ?SHOP_ID_2} + ] +}). + +-spec inspect_user_ok_test(config()) -> _. +inspect_user_ok_test(Config) -> + _ = capi_ct_helper:mock_services( + [ + {inspector, fun( + 'InspectUserShops', + {#'fraudbusters_InspectUserContext'{ + user_info = #'fraudbusters_ClientInfo'{ + email = <<"test@example.com">>, + fingerprint = <<"abc123">>, + ip = <<"192.168.1.1">>, + phone = <<"+79001234567">> + } + }} + ) -> + {ok, #'fraudbusters_BlockedShops'{ + shop_list = [ + #'fraudbusters_ShopContext'{party_id = ?PARTY_ID, shop_id = ?SHOP_ID_1} + ] + }} + end} + ], + Config + ), + _ = capi_ct_helper_bouncer:mock_assert_op_ctx(<<"InspectUser">>, Config), + {ok, #{<<"blockedShops">> := [#{<<"partyID">> := ?PARTY_ID, <<"shopID">> := ?SHOP_ID_1}]}} = + capi_client_antifraud:inspect_user(?config(context, Config), ?INSPECT_USER_REQUEST). + +-spec inspect_user_no_blocked_shops_test(config()) -> _. +inspect_user_no_blocked_shops_test(Config) -> + _ = capi_ct_helper:mock_services( + [ + {inspector, fun('InspectUserShops', _) -> + {ok, #'fraudbusters_BlockedShops'{shop_list = []}} + end} + ], + Config + ), + _ = capi_ct_helper_bouncer:mock_assert_op_ctx(<<"InspectUser">>, Config), + {ok, #{<<"blockedShops">> := []}} = + capi_client_antifraud:inspect_user(?config(context, Config), ?INSPECT_USER_REQUEST). + +-spec inspect_user_invalid_request_test(config()) -> _. +inspect_user_invalid_request_test(Config) -> + _ = capi_ct_helper:mock_services( + [ + {inspector, fun('InspectUserShops', _) -> + {throwing, #'base_InvalidRequest'{errors = [<<"some error">>]}} + end} + ], + Config + ), + _ = capi_ct_helper_bouncer:mock_assert_op_ctx(<<"InspectUser">>, Config), + {error, {400, #{<<"code">> := <<"invalidRequest">>}}} = + capi_client_antifraud:inspect_user(?config(context, Config), ?INSPECT_USER_REQUEST). + +-spec inspect_user_forbidden_test(config()) -> _. +inspect_user_forbidden_test(Config) -> + _ = capi_ct_helper_bouncer:mock_arbiter(capi_ct_helper_bouncer:judge_always_forbidden(), Config), + {error, {401, _}} = + capi_client_antifraud:inspect_user(?config(context, Config), ?INSPECT_USER_REQUEST). diff --git a/apps/capi/test/capi_antifraud_tests_SUITE_data/keys/local/dummy.pem b/apps/capi/test/capi_antifraud_tests_SUITE_data/keys/local/dummy.pem new file mode 100644 index 0000000..059582b --- /dev/null +++ b/apps/capi/test/capi_antifraud_tests_SUITE_data/keys/local/dummy.pem @@ -0,0 +1,13 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXAIBAAKBgQCqGKukO1De7zhZj6+H0qtjTkVxwTCpvKe4eCZ0FPqri0cb2JZfXJ/DgYSF6vUp +wmJG8wVQZKjeGcjDOL5UlsuusFncCzWBQ7RKNUSesmQRMSGkVb1/3j+skZ6UtW+5u09lHNsj6tQ5 +1s1SPrCBkedbNf0Tp0GbMJDyR4e9T04ZZwIDAQABAoGAFijko56+qGyN8M0RVyaRAXz++xTqHBLh +3tx4VgMtrQ+WEgCjhoTwo23KMBAuJGSYnRmoBZM3lMfTKevIkAidPExvYCdm5dYq3XToLkkLv5L2 +pIIVOFMDG+KESnAFV7l2c+cnzRMW0+b6f8mR1CJzZuxVLL6Q02fvLi55/mbSYxECQQDeAw6fiIQX +GukBI4eMZZt4nscy2o12KyYner3VpoeE+Np2q+Z3pvAMd/aNzQ/W9WaI+NRfcxUJrmfPwIGm63il +AkEAxCL5HQb2bQr4ByorcMWm/hEP2MZzROV73yF41hPsRC9m66KrheO9HPTJuo3/9s5p+sqGxOlF +L0NDt4SkosjgGwJAFklyR1uZ/wPJjj611cdBcztlPdqoxssQGnh85BzCj/u3WqBpE2vjvyyvyI5k +X6zk7S0ljKtt2jny2+00VsBerQJBAJGC1Mg5Oydo5NwD6BiROrPxGo2bpTbu/fhrT8ebHkTz2epl +U9VQQSQzY1oZMVX8i1m5WUTLPz2yLJIBQVdXqhMCQBGoiuSoSjafUhV7i1cEGpb88h5NBYZzWXGZ +37sJ5QsW+sJyoNde3xH8vdXhzU7eT82D6X/scw9RZz+/6rCJ4p0= +-----END RSA PRIVATE KEY----- \ No newline at end of file diff --git a/apps/capi/test/capi_antifraud_tests_SUITE_data/keys/local/jwk.json b/apps/capi/test/capi_antifraud_tests_SUITE_data/keys/local/jwk.json new file mode 100644 index 0000000..1b908b2 --- /dev/null +++ b/apps/capi/test/capi_antifraud_tests_SUITE_data/keys/local/jwk.json @@ -0,0 +1,7 @@ +{ + "use": "enc", + "kty": "oct", + "kid": "1111", + "alg": "dir", + "k": "d3JPWmpORzVqbGRrZ2s0aUdjQnJ6ZTh1OW1pdk1kR2Y" +} \ No newline at end of file diff --git a/apps/capi/test/capi_antifraud_tests_SUITE_data/keys/local/jwk.priv.json b/apps/capi/test/capi_antifraud_tests_SUITE_data/keys/local/jwk.priv.json new file mode 100644 index 0000000..e7d6557 --- /dev/null +++ b/apps/capi/test/capi_antifraud_tests_SUITE_data/keys/local/jwk.priv.json @@ -0,0 +1,10 @@ +{ + "use": "enc", + "kty": "EC", + "kid": "kxdD0orVPGoAxWrqAMTeQ0U5MRoK47uZxWiSJdgo0t0", + "crv": "P-256", + "alg": "ECDH-ES", + "x": "nHi7TCgBwfrPuNTf49bGvJMczk6WZOI-mCKAghbrOlM", + "y": "_8kiXGOIWkfz57m8K5dmTfbYzCJVYHZZZisCfbYicr0", + "d": "i45qDiARZ5qbS_uzeT-CiKnPUe64qHitKaVdAvcN6TI" +} \ No newline at end of file diff --git a/apps/capi/test/capi_antifraud_tests_SUITE_data/keys/local/jwk.publ.json b/apps/capi/test/capi_antifraud_tests_SUITE_data/keys/local/jwk.publ.json new file mode 100644 index 0000000..00b7002 --- /dev/null +++ b/apps/capi/test/capi_antifraud_tests_SUITE_data/keys/local/jwk.publ.json @@ -0,0 +1,9 @@ +{ + "use": "enc", + "kty": "EC", + "kid": "kxdD0orVPGoAxWrqAMTeQ0U5MRoK47uZxWiSJdgo0t0", + "crv": "P-256", + "alg": "ECDH-ES", + "x": "nHi7TCgBwfrPuNTf49bGvJMczk6WZOI-mCKAghbrOlM", + "y": "_8kiXGOIWkfz57m8K5dmTfbYzCJVYHZZZisCfbYicr0" +} \ No newline at end of file diff --git a/apps/capi/test/capi_antifraud_tests_SUITE_data/keys/local/private.pem b/apps/capi/test/capi_antifraud_tests_SUITE_data/keys/local/private.pem new file mode 100644 index 0000000..4e6d12c --- /dev/null +++ b/apps/capi/test/capi_antifraud_tests_SUITE_data/keys/local/private.pem @@ -0,0 +1,9 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIBOwIBAAJBAK9fx7qOJT7Aoseu7KKgaLagBh3wvDzg7F/ZMtGbPFikJnnvRWvF +B5oEGbMPblvtF0/fjqfu+eqjP3Z1tUSn7TkCAwEAAQJABUY5KIgr4JZEjwLYxQ9T +9uIbLP1Xe/E7yqoqmBk2GGhSrPY0OeRkYnUVLcP96UPQhF63iuG8VF6uZ7oAPsq+ +gQIhANZy3jSCzPjXYHRU1kRqQzpt2S+OqoEiqQ6YG1HrC/VxAiEA0Vq6JlQK2tOX +37SS00dK0Qog4Qi8dN73GliFQNP18EkCIQC4epSA48zkfJMzQBAbRraSuxDNApPX +BzQbo+pMrEDbYQIgY4AncQgIkLB4Qk5kah48JNYXglzQlQtTjiX8Ty9ueGECIQCM +GD3UbQKiA0gf5plBA24I4wFVKxxa4wXbW/7SfP6XmQ== +-----END RSA PRIVATE KEY----- diff --git a/apps/capi/test/capi_antifraud_tests_SUITE_data/keys/local/public.pem b/apps/capi/test/capi_antifraud_tests_SUITE_data/keys/local/public.pem new file mode 100644 index 0000000..c2f50d4 --- /dev/null +++ b/apps/capi/test/capi_antifraud_tests_SUITE_data/keys/local/public.pem @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAK9fx7qOJT7Aoseu7KKgaLagBh3wvDzg +7F/ZMtGbPFikJnnvRWvFB5oEGbMPblvtF0/fjqfu+eqjP3Z1tUSn7TkCAwEAAQ== +-----END PUBLIC KEY----- diff --git a/apps/capi_client/src/capi_client_antifraud.erl b/apps/capi_client/src/capi_client_antifraud.erl new file mode 100644 index 0000000..f5bd8c8 --- /dev/null +++ b/apps/capi_client/src/capi_client_antifraud.erl @@ -0,0 +1,14 @@ +-module(capi_client_antifraud). + +-export([inspect_user/2]). + +-type context() :: capi_client_lib:context(). + +-spec inspect_user(context(), map()) -> {ok, term()} | {error, term()}. +inspect_user(Context, Request) -> + Params = #{ + body => Request + }, + {Url, PreparedParams, Opts} = capi_client_lib:make_request(Context, Params), + Response = swag_client_inspector_api:inspect_user(Url, PreparedParams, Opts), + capi_client_lib:handle_response(Response). diff --git a/apps/capi_woody_client/src/capi_woody_client.erl b/apps/capi_woody_client/src/capi_woody_client.erl index 2a14143..d76a0e6 100644 --- a/apps/capi_woody_client/src/capi_woody_client.erl +++ b/apps/capi_woody_client/src/capi_woody_client.erl @@ -73,6 +73,8 @@ apply_retry_step({wait, Timeout, Retry}, Deadline0, Error) -> end. -spec get_service_modname(service_name()) -> woody:service(). +get_service_modname(inspector) -> + {fb_proto_fraudbusters_thrift, 'InspectorService'}; get_service_modname(invoicing) -> {dmsl_payproc_thrift, 'Invoicing'}; get_service_modname(invoice_templating) -> diff --git a/config/sys.config b/config/sys.config index c39d1a1..2e41b7d 100644 --- a/config/sys.config +++ b/config/sys.config @@ -96,7 +96,8 @@ party_management => <<"http://hellgate:8022/v1/processing/partymgmt">>, invoicing => <<"http://hellgate:8022/v1/processing/invoicing">>, invoice_templating => <<"http://hellgate:8022/v1/processing/invoice_templating">>, - webhook_manager => <<"http://hooker:8022/hook">> + webhook_manager => <<"http://hooker:8022/hook">>, + inspector => <<"http://fraudbusters:8022/v1/inspector">> }}, {service_deadlines, #{ % milliseconds diff --git a/rebar.config b/rebar.config index 6d1c3df..789a130 100644 --- a/rebar.config +++ b/rebar.config @@ -54,6 +54,7 @@ %% Libraries generated with swagger-codegen-erlang from valitydev/swag-payments {swag_server, {git, "https://github.com/valitydev/swag-payments", {branch, "release/erlang/server/v3"}}}, {swag_client, {git, "https://github.com/valitydev/swag-payments", {branch, "release/erlang/client/v3"}}}, + {fraudbusters_proto, {git, "https://github.com/valitydev/fraudbusters-proto.git", {branch, "master"}}}, %% OpenTelemetry deps {opentelemetry_api, "1.4.0"}, {opentelemetry, "1.5.0"}, diff --git a/rebar.lock b/rebar.lock index 7d1af3e..1481352 100644 --- a/rebar.lock +++ b/rebar.lock @@ -56,6 +56,10 @@ {git,"https://github.com/valitydev/feat.git", {ref,"bf7dff68c822e58769da962e7f99c3e428a88551"}}, 0}, + {<<"fraudbusters_proto">>, + {git,"https://github.com/valitydev/fraudbusters-proto.git", + {ref,"6b26fc1463bdc9f2d0f6bebe482249c59d0b4259"}}, + 0}, {<<"genlib">>, {git,"https://github.com/valitydev/genlib.git", {ref,"d2324089afbbd9630e85fac554620f1de0b33dfe"}}, @@ -122,11 +126,11 @@ {<<"ssl_verify_fun">>,{pkg,<<"ssl_verify_fun">>,<<"1.1.7">>},2}, {<<"swag_client">>, {git,"https://github.com/valitydev/swag-payments", - {ref,"24d442d4e14eb8ccb7a960fcebdf9a388c2d4700"}}, + {ref,"34f093017def0b15e5586197df76e6e66ae77c2a"}}, 0}, {<<"swag_server">>, {git,"https://github.com/valitydev/swag-payments", - {ref,"fddf13c86f824c75d9750cec90c9c742c7a377e7"}}, + {ref,"fca7721f235be2f94ad3b6e9c675b284197231fc"}}, 0}, {<<"thrift">>, {git,"https://github.com/valitydev/thrift_erlang.git",