diff --git a/Jenkinsfile b/Jenkinsfile index c11c332..924e3cc 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -41,7 +41,7 @@ build('fistful-server', 'docker-host', finalHook) { } runStage('dialyze') { - withWsCache("_build/default/rebar3_21.3.8.7_plt") { + withWsCache("_build/default/rebar3_22.3.1_plt") { sh 'make wc_dialyze' } } diff --git a/Makefile b/Makefile index 4bd9198..7dd2ba5 100644 --- a/Makefile +++ b/Makefile @@ -20,7 +20,7 @@ BASE_IMAGE_NAME := service-erlang BASE_IMAGE_TAG := da0ab769f01b650b389d18fc85e7418e727cbe96 # Build image tag to be used -BUILD_IMAGE_TAG := 4536c31941b9c27c134e8daf0fd18848809219c9 +BUILD_IMAGE_TAG := 442c2c274c1d8e484e5213089906a4271641d95e REGISTRY := dr2.rbkmoney.com diff --git a/apps/ff_cth/src/ct_helper.erl b/apps/ff_cth/src/ct_helper.erl index ff0fa33..5c05e53 100644 --- a/apps/ff_cth/src/ct_helper.erl +++ b/apps/ff_cth/src/ct_helper.erl @@ -112,6 +112,11 @@ start_app(wapi = AppName) -> decryption_key_paths => [ "/opt/wapi/config/jwk.json" ] + }}, + {swagger_handler_opts, #{ + validation_opts => #{ + custom_validator => wapi_swagger_validator + } }} ]), #{}}; diff --git a/apps/wapi/src/wapi_sup.erl b/apps/wapi/src/wapi_sup.erl index f931bce..eb82cf2 100644 --- a/apps/wapi/src/wapi_sup.erl +++ b/apps/wapi/src/wapi_sup.erl @@ -28,7 +28,8 @@ init([]) -> {LogicHandlers, LogicHandlerSpecs} = get_logic_handler_info(), HealthCheck = enable_health_logging(genlib_app:env(wapi, health_check, #{})), HealthRoutes = [{'_', [erl_health_handle:get_route(HealthCheck)]}], - SwaggerSpec = wapi_swagger_server:child_spec(HealthRoutes, LogicHandlers), + SwaggerHandlerOpts = genlib_app:env(wapi, swagger_handler_opts, #{}), + SwaggerSpec = wapi_swagger_server:child_spec(HealthRoutes, LogicHandlers, SwaggerHandlerOpts), {ok, { {one_for_all, 0, 1}, [LechiffreSpec] ++ diff --git a/apps/wapi/src/wapi_swagger_server.erl b/apps/wapi/src/wapi_swagger_server.erl index 84798ab..829f154 100644 --- a/apps/wapi/src/wapi_swagger_server.erl +++ b/apps/wapi/src/wapi_swagger_server.erl @@ -1,6 +1,6 @@ -module(wapi_swagger_server). --export([child_spec/2]). +-export([child_spec/3]). -export_type([logic_handler/0]). -export_type([logic_handlers/0]). @@ -8,16 +8,18 @@ -type logic_handler() :: swag_server_wallet:logic_handler(_). -type logic_handlers() :: #{atom() => logic_handler()}. +-type swagger_handler_opts() :: swag_server_wallet_router:swagger_handler_opts(). + -define(APP, wapi). -define(DEFAULT_ACCEPTORS_POOLSIZE, 100). -define(DEFAULT_IP_ADDR, "::"). -define(DEFAULT_PORT, 8080). --spec child_spec(cowboy_router:routes(), logic_handlers()) -> +-spec child_spec(cowboy_router:routes(), logic_handlers(), swagger_handler_opts()) -> supervisor:child_spec(). -child_spec(HealthRoutes, LogicHandlers) -> +child_spec(HealthRoutes, LogicHandlers, SwaggerHandlerOpts) -> {Transport, TransportOpts} = get_socket_transport(), - CowboyOpts = get_cowboy_config(HealthRoutes, LogicHandlers), + CowboyOpts = get_cowboy_config(HealthRoutes, LogicHandlers, SwaggerHandlerOpts), ranch:child_spec(?MODULE, Transport, TransportOpts, cowboy_clear, CowboyOpts). get_socket_transport() -> @@ -26,11 +28,14 @@ get_socket_transport() -> AcceptorsPool = genlib_app:env(?APP, acceptors_poolsize, ?DEFAULT_ACCEPTORS_POOLSIZE), {ranch_tcp, #{socket_opts => [{ip, IP}, {port, Port}], num_acceptors => AcceptorsPool}}. -get_cowboy_config(HealthRoutes, LogicHandlers) -> +get_cowboy_config(HealthRoutes, LogicHandlers, SwaggerHandlerOpts) -> Dispatch = cowboy_router:compile(squash_routes( HealthRoutes ++ - swag_server_wallet_router:get_paths(maps:get(wallet, LogicHandlers)) + swag_server_wallet_router:get_paths( + maps:get(wallet, LogicHandlers), + SwaggerHandlerOpts + ) )), CowboyOpts = #{ env => #{ diff --git a/apps/wapi/src/wapi_swagger_validator.erl b/apps/wapi/src/wapi_swagger_validator.erl new file mode 100644 index 0000000..5087d1d --- /dev/null +++ b/apps/wapi/src/wapi_swagger_validator.erl @@ -0,0 +1,80 @@ +-module(wapi_swagger_validator). + +-type param_rule() :: swag_server_wallet_param_validator:param_rule(). +-type schema_rule() :: swag_server_wallet_schema_validator:schema_rule(). +-type value() :: swag_server_wallet:value(). +-type param_context() :: swag_server_wallet_param_validator:context(). +-type schema_context() :: swag_server_wallet_schema_validator:context(). + +-type validate_param_result() :: + ok | {ok, term()} | pass | error | {error, Error :: term()}. + +-type validate_schema_result() :: + jesse_state:state() | pass | no_return(). + +-behaviour(swag_server_wallet_custom_validator). + +-export([validate_param/3]). +-export([validate_schema/4]). + +-spec validate_param(param_rule(), value(), param_context()) -> + validate_param_result(). +validate_param(_Rule, _Value, _Meta) -> + pass. + +-spec validate_schema(schema_rule(), value(), schema_context(), jesse_state:state()) -> + validate_schema_result(). + +validate_schema( + {<<"type">>, <<"string">>}, + Value, + #{ + operation_id := 'CreateDestination', + definition_name := 'Destination', + % current_path := [<<"name">>], % check all fields + msg_type := request + }, + JesseState +) when is_binary(Value) -> + case check_destination_name(Value) of + ok -> + pass; % pass back to the built-in validator + error -> + jesse_error:handle_data_invalid(wrong_format, Value, JesseState) + end; +validate_schema(_Rule, _Value, _Meta, _JesseState) -> + pass. + +check_destination_name(Name) -> + case re:run(Name, <<"\\d{12,19}">>, [{capture, all, binary}, global]) of + nomatch -> ok; + {match, Captured} -> check_luhn(Captured) + end. + +check_luhn([]) -> + ok; +check_luhn([Captured | Rest]) -> + case lists:any(fun do_check_luhn/1, Captured) of + true -> error; + false -> check_luhn(Rest) + end. + +do_check_luhn(String) -> + do_check_luhn(String, 0). + +do_check_luhn(<>, Sum) -> + case Sum * 9 rem 10 of + M when M =:= CheckSum - $0 -> + true; + _M -> + false + end; +do_check_luhn(<>, Sum) when byte_size(Rest) rem 2 =:= 1 -> + case (N - $0) * 2 of + M when M >= 10 -> + do_check_luhn(Rest, Sum + M div 10 + M rem 10); + M -> + do_check_luhn(Rest, Sum + M) + end; +do_check_luhn(<>, Sum) -> + do_check_luhn(Rest, Sum + N - $0). diff --git a/apps/wapi/src/wapi_wallet_ff_backend.erl b/apps/wapi/src/wapi_wallet_ff_backend.erl index 2ba36de..6b4391a 100644 --- a/apps/wapi/src/wapi_wallet_ff_backend.erl +++ b/apps/wapi/src/wapi_wallet_ff_backend.erl @@ -357,7 +357,6 @@ create_destination(Params = #{<<"identity">> := IdenityId}, Context) -> CreateFun = fun(ID, EntityCtx) -> _ = check_resource(identity, IdenityId, Context), DestinationParams = from_swag(destination_params, Params), - _ = check_destination_params(DestinationParams), Resource = unwrap(construct_resource(maps:get(resource, DestinationParams))), ff_destination:create( DestinationParams#{id => ID, resource => Resource}, @@ -841,13 +840,6 @@ when Type =:= <<"CryptoWalletDestinationResource">> -> currency => from_swag(crypto_wallet_currency, Resource) })}}}. -%%@TODO delete as soon as a more permanent solution is in place -check_destination_params(#{name := Name}) -> - case re:run(Name, <<"\\d{12,19}">>, [{capture, none}]) of - nomatch -> ok; - match -> throw({illegal_pattern, name}) - end. - encode_resource_bank_card(BankCard, AuthData) -> EncodedBankCard = encode_bank_card(BankCard), {bank_card, EncodedBankCard#{auth_data => {session, #{session_id => AuthData}}}}. diff --git a/apps/wapi/src/wapi_wallet_handler.erl b/apps/wapi/src/wapi_wallet_handler.erl index 99d308e..8b90226 100644 --- a/apps/wapi/src/wapi_wallet_handler.erl +++ b/apps/wapi/src/wapi_wallet_handler.erl @@ -272,12 +272,6 @@ process_request('CreateDestination', #{'Destination' := Params}, Context, Opts) wapi_handler_utils:logic_error(external_id_conflict, {ID, ExternalID}); {error, invalid} -> wapi_handler_utils:reply_ok(422, wapi_handler_utils:get_error_msg(<<"Invalid currency">>)); - {error, {illegal_pattern, Name}} -> - wapi_handler_utils:reply_error(400, #{ - <<"errorType">> => <<"SchemaViolated">>, - <<"name">> => Name, - <<"description">> => <<"Illegal pattern found in parameter">> - }); {error, {invalid_resource_token, Type}} -> wapi_handler_utils:reply_error(400, #{ <<"errorType">> => <<"InvalidResourceToken">>, diff --git a/apps/wapi/test/wapi_SUITE.erl b/apps/wapi/test/wapi_SUITE.erl index 064cc79..1f83771 100644 --- a/apps/wapi/test/wapi_SUITE.erl +++ b/apps/wapi/test/wapi_SUITE.erl @@ -173,11 +173,25 @@ create_destination_failed_test(C) -> {error, {400, #{<<"errorType">> := <<"InvalidResourceToken">>}}} = create_destination(IdentityID, Resource0, C), %% - DestinationName = <<"4242424242424242">>, + DestinationName0 = <<"abc4242424242424242">>, CardToken = store_bank_card(C), Resource1 = make_bank_card_resource(CardToken), - {error, {400, #{<<"errorType">> := <<"SchemaViolated">>}}} - = create_destination(DestinationName, IdentityID, Resource1, C). + {error, {response_validation_failed, _, + #{ + <<"errorType">> := <<"schema_violated">>, + <<"name">> := <<"Destination">> + } + }} = create_destination(DestinationName0, IdentityID, Resource1, C), + DestinationName1 = <<"abc1231241241241244">>, + IdentityID1 = <<"4242424242424242">>, + {error, {response_validation_failed, _, + #{ + <<"errorType">> := <<"schema_violated">>, + <<"name">> := <<"Destination">> + } + }} = create_destination(DestinationName1, IdentityID1, Resource1, C), + DestinationName2 = <<"1231241241241244">>, + {ok, _} = create_destination(DestinationName2, IdentityID, Resource1, C). -spec withdrawal_to_bank_card_test(config()) -> test_return(). diff --git a/rebar.lock b/rebar.lock index 2d33a95..d59ef79 100644 --- a/rebar.lock +++ b/rebar.lock @@ -45,6 +45,10 @@ {git,"git@github.com:rbkmoney/dmt_core.git", {ref,"8ac78cb1c94abdcdda6675dd7519893626567573"}}, 1}, + {<<"email_validator">>, + {git,"https://github.com/rbkmoney/email_validator.git", + {ref,"be90c6ebd34d29fa9390136469b99d8a68ad4996"}}, + 0}, {<<"erl_health">>, {git,"https://github.com/rbkmoney/erlang-health.git", {ref,"c190cb8de0359b933a27cd20ddc74180c0e5f5c4"}},