Nail wapi app here for a while

This commit is contained in:
Anton Belyaev 2018-07-06 16:49:30 +03:00 committed by GitHub
parent db02893b11
commit 623197fba5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 3265 additions and 23 deletions

4
.gitignore vendored
View File

@ -6,9 +6,13 @@ log
erl_crash.dump
.tags*
*.sublime-workspace
.edts
.DS_Store
Dockerfile
docker-compose.yml
/.idea/
*.beam
/test/log/
# wapi
apps/swag_*

4
.gitmodules vendored
View File

@ -1,3 +1,7 @@
[submodule "build-utils"]
path = build-utils
url = git+ssh://github.com/rbkmoney/build_utils
[submodule "schemes/swag"]
path = schemes/swag
url = git@github.com:rbkmoney/swag-wallets.git
branch = release/v0

View File

@ -5,6 +5,10 @@ MAINTAINER Andrey Mayorov <a.mayorov@rbkmoney.com>
COPY ./_build/prod/rel/${SERVICE_NAME} /opt/${SERVICE_NAME}
CMD /opt/${SERVICE_NAME}/bin/${SERVICE_NAME} foreground
EXPOSE 8022
# wapi
EXPOSE 8080
# A bit of magic below to get a proper branch name
# even when the HEAD is detached (Hey Jenkins!
# BRANCH_NAME is available in Jenkins env).

View File

@ -1,5 +1,8 @@
REBAR := $(or $(shell which rebar3), $(error "`rebar3' executable missing"))
REBAR = $(or $(shell which rebar3), $(error "`rebar3' executable missing"))
SUBMODULES = build-utils
# wapi
SUBMODULES += schemes/swag
SUBTARGETS = $(patsubst %,%/.git,$(SUBMODULES))
UTILS_PATH := build-utils
@ -17,9 +20,10 @@ BASE_IMAGE_NAME := service_erlang
BASE_IMAGE_TAG := 16e2b3ef17e5fdefac8554ced9c2c74e5c6e9e11
# Build image tag to be used
BUILD_IMAGE_TAG := 562313697353c29d4b34fb081a8b70e8c2207134
BUILD_IMAGE_TAG := 585ec439a97bade30cfcebc36cefdb45f13f3372
CALL_ANYWHERE := all submodules compile xref lint dialyze release clean distclean
CALL_ANYWHERE += generate regenerate
CALL_W_CONTAINER := $(CALL_ANYWHERE) test
@ -36,7 +40,7 @@ $(SUBTARGETS): %/.git: %
submodules: $(SUBTARGETS)
compile: submodules
compile: submodules generate
$(REBAR) compile
xref: submodules
@ -54,12 +58,12 @@ release: submodules
clean:
$(REBAR) clean
distclean:
distclean: swag_server.distclean swag_client.distclean
$(REBAR) clean -a
rm -rf _build
test: submodules
$(REBAR) ct
$(REBAR) eunit ct
TESTSUITES = $(wildcard apps/*/test/*_SUITE.erl)
@ -71,3 +75,74 @@ test.$(patsubst ff_%_SUITE.erl,%,$(notdir $(1))): $(1) submodules
endef
$(foreach suite,$(TESTSUITES),$(eval $(call testsuite,$(suite))))
#
# wapi
#
.PHONY: generate regenerate swag_server.generate swag_server.regenerate swag_client.generate swag_client.regenerate
generate: swag_server.generate swag_client.generate
regenerate: swag_server.regenerate swag_client.regenerate
SWAGGER_CODEGEN = $(call which, swagger-codegen)
SWAGGER_SCHEME_BASE_PATH := schemes/swag
APP_PATH := apps
SWAGGER_SCHEME_API_PATH := $(SWAGGER_SCHEME_BASE_PATH)/api
SWAG_SPEC_FILE := swagger.yaml
# Swagger server
SWAG_SERVER_PREFIX := swag_server
SWAG_SERVER_APP_TARGET := $(APP_PATH)/$(SWAG_SERVER_PREFIX)
SWAG_SERVER_APP_PATH := $(APP_PATH)/$(SWAG_SERVER_PREFIX)
SWAG_SERVER_APP_TARGET_WALLET := $(SWAG_SERVER_APP_PATH)_wallet/rebar.config
SWAG_SERVER_APP_TARGET_PAYRES := $(SWAG_SERVER_APP_PATH)_payres/rebar.config
SWAG_SERVER_APP_TARGET_PRIVDOC := $(SWAG_SERVER_APP_PATH)_privdoc/rebar.config
$(SWAG_SERVER_APP_PATH)_%/rebar.config: $(SWAGGER_SCHEME_BASE_PATH)/.git
$(SWAGGER_CODEGEN) generate \
-i $(SWAGGER_SCHEME_API_PATH)/$*/$(SWAG_SPEC_FILE) \
-l erlang-server \
-o $(SWAG_SERVER_APP_PATH)_$* \
--additional-properties \
packageName=$(SWAG_SERVER_PREFIX)_$*
swag_server.generate: $(SWAG_SERVER_APP_TARGET_WALLET) $(SWAG_SERVER_APP_TARGET_PAYRES) $(SWAG_SERVER_APP_TARGET_PRIVDOC)
swag_server.distclean: swag_server.distclean_wallet swag_server.distclean_payres swag_server.distclean_privdoc
swag_server.distclean_%:
rm -rf $(SWAG_SERVER_APP_PATH)_$*
swag_server.regenerate: swag_server.distclean swag_server.generate
# Swagger client
SWAG_CLIENT_PREFIX := swag_client
SWAG_CLIENT_APP_TARGET := $(APP_PATH)/$(SWAG_CLIENT_PREFIX)
SWAG_CLIENT_APP_PATH := $(APP_PATH)/$(SWAG_CLIENT_PREFIX)
SWAG_CLIENT_APP_TARGET_WALLET := $(SWAG_CLIENT_APP_PATH)_wallet/rebar.config
SWAG_CLIENT_APP_TARGET_PAYRES := $(SWAG_CLIENT_APP_PATH)_payres/rebar.config
SWAG_CLIENT_APP_TARGET_PRIVDOC := $(SWAG_CLIENT_APP_PATH)_privdoc/rebar.config
$(SWAG_CLIENT_APP_PATH)_%/rebar.config: $(SWAGGER_SCHEME_BASE_PATH)/.git
$(SWAGGER_CODEGEN) generate \
-i $(SWAGGER_SCHEME_API_PATH)/$*/$(SWAG_SPEC_FILE) \
-l erlang-client \
-o $(SWAG_CLIENT_APP_PATH)_$* \
--additional-properties \
packageName=$(SWAG_CLIENT_PREFIX)_$*
swag_client.generate: $(SWAG_CLIENT_APP_TARGET_WALLET) $(SWAG_CLIENT_APP_TARGET_PAYRES) $(SWAG_CLIENT_APP_TARGET_PRIVDOC)
swag_client.distclean: swag_client.distclean_wallet swag_client.distclean_payres swag_client.distclean_privdoc
swag_client.distclean_%:
rm -rf $(SWAG_CLIENT_APP_PATH)_$*
swag_client.regenerate: swag_client.distclean swag_client.generate

View File

@ -10,7 +10,8 @@
stdlib,
woody,
fistful,
ff_withdraw
ff_withdraw,
wapi
]},
{env, []},
{modules, []},

View File

@ -98,12 +98,12 @@ get(ID) ->
-type event_cursor() :: non_neg_integer() | undefined.
-spec get_status_events(id(), event_cursor()) ->
{ok, [ev()]} |
{error, notfound} .
{ok, [{integer(), machinery:timestamp(), ev()}]} |
{error, notfound} .
get_status_events(ID, Cursor) ->
do(fun () ->
unwrap(machinery:get(?NS, ID, {Cursor, undefined, forward}, backend()))
maps:get(history, unwrap(machinery:get(?NS, ID, {Cursor, undefined, forward}, backend())))
end).
backend() ->

View File

@ -20,6 +20,7 @@
-export_type([ctx/0]).
-export([new/0]).
-export([get/2]).
%%
@ -28,3 +29,10 @@
new() ->
#{}.
-spec get(namespace(), ctx()) ->
{ok, md()} |
{error, notfound}.
get(Ns, Ctx) ->
ff_map:find(Ns, Ctx).

View File

@ -55,6 +55,7 @@
-export([provider/1]).
-export([party/1]).
-export([class/1]).
-export([level/1]).
-export([contract/1]).
-export([challenge/2]).
-export([effective_challenge/1]).

View File

@ -102,7 +102,7 @@ get(ID) ->
-type challenge_params() :: #{
id := challenge_id(),
class := ff_identity_class:challenge_class_id(),
proofs := [ff_identity:proof()]
proofs := [ff_identity_challenge:proof()]
}.
-spec start_challenge(id(), challenge_params()) ->

77
apps/wapi/elvis.config Normal file
View File

@ -0,0 +1,77 @@
[
{elvis, [
{config, [
#{
dirs => [
"apps/*/src",
"apps/*/test"
],
filter => "*.erl",
ignore => ["_thrift.erl$", "src/swag_server*", "src/swag_client*", "_SUITE.erl$"],
rules => [
{elvis_style, line_length, #{limit => 120, skip_comments => false}},
{elvis_style, no_tabs},
{elvis_style, no_trailing_whitespace},
{elvis_style, macro_module_names},
{elvis_style, operator_spaces, #{rules => [{right, ","}, {right, "++"}, {left, "++"}]}},
{elvis_style, nesting_level, #{level => 4}},
{elvis_style, god_modules, #{limit => 25}},
{elvis_style, no_if_expression},
{elvis_style, invalid_dynamic_call, #{ignore => [wapi_swagger_server]}},
{elvis_style, used_ignored_variable},
{elvis_style, no_behavior_info},
{elvis_style, module_naming_convention, #{regex => "^[a-z]([a-z0-9]*_?)*(_SUITE)?$"}},
{elvis_style, function_naming_convention, #{regex => "^[a-z]([a-z0-9]*_?)*$"}},
{elvis_style, state_record_and_type},
{elvis_style, no_spec_with_records},
{elvis_style, dont_repeat_yourself, #{
min_complexity => 30,
ignore => [
wapi_tests_SUITE
]
}},
{elvis_style, no_debug_call, #{}}
]
},
#{
dirs => ["."],
filter => "Makefile",
ruleset => makefiles
},
#{
dirs => ["."],
filter => "elvis.config",
ruleset => elvis_config
},
#{
dirs => ["apps", "apps/*"],
filter => "rebar.config",
ignore => ["swag_server/*", "swag_client/*"],
rules => [
{elvis_style, line_length, #{limit => 120, skip_comments => false}},
{elvis_style, no_tabs},
{elvis_style, no_trailing_whitespace}
]
},
#{
dirs => ["."],
filter => "rebar.config",
rules => [
{elvis_style, line_length, #{limit => 120, skip_comments => false}},
{elvis_style, no_tabs},
{elvis_style, no_trailing_whitespace}
]
},
#{
dirs => ["apps/*/src"],
filter => "*.app.src",
ignore => ["src/swag_server*", "src/swag_client*"],
rules => [
{elvis_style, line_length, #{limit => 120, skip_comments => false}},
{elvis_style, no_tabs},
{elvis_style, no_trailing_whitespace}
]
}
]}
]}
].

129
apps/wapi/rebar.config Normal file
View File

@ -0,0 +1,129 @@
%% Common project erlang options.
{erl_opts, [
%% % mandatory
%% debug_info,
%% warnings_as_errors,
%% warn_export_all,
%% warn_missing_spec,
%% warn_untyped_record,
%% warn_export_vars,
%% % by default
%% warn_unused_record,
%% warn_bif_clash,
%% warn_obsolete_guard,
%% warn_unused_vars,
%% warn_shadow_vars,
%% warn_unused_import,
%% warn_unused_function,
%% warn_deprecated_function,
%% % at will
%% % bin_opt_info
%% % no_auto_import
%% % warn_missing_spec_all
{parse_transform, lager_transform}
]}.
%% Common project dependencies.
{deps, [
{cowboy, "1.0.4"},
%% {rfc3339, "0.2.2"},
{jose, "1.7.9"},
%% {lager, "3.6.1"},
{base64url, "0.0.1"},
%% {genlib,
%% {git, "https://github.com/rbkmoney/genlib.git", {branch, "master"}}
%% },
%% {woody,
%% {git, "git@github.com:rbkmoney/woody_erlang.git", {branch, "master"}}
%% },
%% {woody_user_identity,
%% {git, "git@github.com:rbkmoney/woody_erlang_user_identity.git", {branch, "master"}}
%% },
%% {dmsl,
%% {git, "git@github.com:rbkmoney/damsel.git", {branch, "release/erlang/master"}}
%% },
%% {lager_logstash_formatter,
%% {git, "git@github.com:rbkmoney/lager_logstash_formatter.git", {branch, "master"}}
%% },
{cowboy_cors,
{git, "https://github.com/danielwhite/cowboy_cors.git", {branch, "master"}}
},
{cowboy_access_log,
{git, "git@github.com:rbkmoney/cowboy_access_log.git", {branch, "master"}}
},
{payproc_errors,
{git, "git@github.com:rbkmoney/payproc-errors-erlang.git", {branch, "master"}}
},
{erl_health,
{git, "https://github.com/rbkmoney/erlang-health.git", {branch, master}}
}
]}.
%% XRef checks
%% {xref_checks, [
%% undefined_function_calls,
%% undefined_functions,
%% deprecated_functions_calls,
%% deprecated_functions
%% ]}.
% at will
% {xref_warnings, true}.
%% Tests
%% {cover_enabled, true}.
%% Relx configuration
%% {relx, [
%% {release, { capi , "0.1.0"}, [
%% {recon , load }, % tools for introspection
%% {runtime_tools, load }, % debugger
%% {tools , load }, % profiler
%% capi,
%% sasl
%% ]},
%% {sys_config, "./config/sys.config"},
%% {vm_args, "./config/vm.args"},
%% {dev_mode, true},
%% {include_erts, false},
%% {extended_start_script, true}
%% ]}.
%% Dialyzer static analyzing
%% {dialyzer, [
%% {warnings, [
%% % mandatory
%% unmatched_returns,
%% error_handling,
%% race_conditions,
%% unknown
%% ]},
%% {plt_apps, all_deps}
%% ]}.
%% {profiles, [
%% {prod, [
%% {deps, [
%% % for introspection on production
%% {recon, "2.3.2"}
%% ]},
%% {relx, [
%% {dev_mode, false},
%% {include_erts, true},
%% {overlay, [
%% {mkdir , "var/keys/capi" },
%% {copy , "var/keys/capi/private.pem" , "var/keys/capi/private.pem" }
%% ]}
%% ]}
%% ]},
%% {test, [
%% {cover_enabled, true},
%% {deps, []}
%% ]}
%% ]}.
%% {pre_hooks, [
%% {thrift, "git submodule update --init"}
%% ]}.

View File

@ -0,0 +1,31 @@
{application, wapi , [
{description, "Wallet API service adapter"},
{vsn, "0.1.0"},
{registered, []},
{mod, {wapi , []}},
{applications, [
kernel,
stdlib,
public_key,
lager,
%% lager_logstash_formatter,
genlib,
woody,
scoper,
dmsl,
swag_server_wallet,
swag_server_payres,
swag_server_privdoc,
jose,
cowboy_cors,
cowboy_access_log,
rfc3339,
base64url,
snowflake,
woody_user_identity,
payproc_errors,
erl_health,
identdocstore_proto
]},
{env, []}
]}.

21
apps/wapi/src/wapi.erl Normal file
View File

@ -0,0 +1,21 @@
%% @doc Public API and application startup.
%% @end
-module(wapi).
-behaviour(application).
%% Application callbacks
-export([start/2]).
-export([stop/1]).
%%
-spec start(normal, any()) -> {ok, pid()} | {error, any()}.
start(_StartType, _StartArgs) ->
wapi_sup:start_link().
-spec stop(any()) -> ok.
stop(_State) ->
ok.

241
apps/wapi/src/wapi_acl.erl Normal file
View File

@ -0,0 +1,241 @@
-module(wapi_acl).
%%
-opaque t() :: [{{priority(), scope()}, [permission()]}].
-type priority() :: integer().
-type scope() :: [resource() | {resource(), resource_id()}, ...].
-type resource() :: atom().
-type resource_id() :: binary().
-type permission() :: read | write.
-export_type([t/0]).
-export_type([scope/0]).
-export_type([permission/0]).
-export([new/0]).
-export([to_list/1]).
-export([from_list/1]).
-export([insert_scope/3]).
-export([remove_scope/3]).
-export([match/2]).
-export([decode/1]).
-export([encode/1]).
%%
-spec new() ->
t().
new() ->
[].
-spec to_list(t()) ->
[{scope(), permission()}].
to_list(ACL) ->
[{S, P} || {{_, S}, P} <- ACL].
-spec from_list([{scope(), permission()}]) ->
t().
from_list(L) ->
lists:foldl(fun ({S, P}, ACL) -> insert_scope(S, P, ACL) end, new(), L).
-spec insert_scope(scope(), permission(), t()) ->
t().
insert_scope(Scope, Permission, ACL) ->
Priority = compute_priority(Scope, Permission),
insert({{Priority, Scope}, [Permission]}, ACL).
insert({PS, _} = V, [{PS0, _} = V0 | Vs]) when PS < PS0 ->
[V0 | insert(V, Vs)];
insert({PS, Perms}, [{PS, Perms0} | Vs]) ->
% NOTE squashing permissions of entries with the same scope
[{PS, lists:usort(Perms ++ Perms0)} | Vs];
insert({PS, _} = V, [{PS0, _} | _] = Vs) when PS > PS0 ->
[V | Vs];
insert(V, []) ->
[V].
-spec remove_scope(scope(), permission(), t()) ->
t().
remove_scope(Scope, Permission, ACL) ->
Priority = compute_priority(Scope, Permission),
remove({{Priority, Scope}, [Permission]}, ACL).
remove(V, [V | Vs]) ->
Vs;
remove({PS, Perms}, [{PS, Perms0} | Vs]) ->
[{PS, Perms0 -- Perms} | Vs];
remove(V, [V0 | Vs]) ->
[V0 | remove(V, Vs)];
remove(_, []) ->
[].
compute_priority(Scope, Permission) ->
% NOTE
% Scope priority depends on the following attributes, in the order of decreasing
% importance:
% 1. Depth, deeper is more important
% 2. Scope element specificity, element marked with an ID is more important
compute_scope_priority(Scope) + compute_permission_priority(Permission).
compute_scope_priority(Scope) when length(Scope) > 0 ->
compute_scope_priority(Scope, get_resource_hierarchy(), 0);
compute_scope_priority(Scope) ->
error({badarg, {scope, Scope}}).
compute_scope_priority([{Resource, _ID} | Rest], H, P) ->
compute_scope_priority(Rest, delve(Resource, H), P * 10 + 2);
compute_scope_priority([Resource | Rest], H, P) ->
compute_scope_priority(Rest, delve(Resource, H), P * 10 + 1);
compute_scope_priority([], _, P) ->
P * 10.
compute_permission_priority(read) ->
0;
compute_permission_priority(write) ->
0;
compute_permission_priority(V) ->
error({badarg, {permission, V}}).
%%
-spec match(scope(), t()) ->
[permission()].
match(Scope, ACL) when length(Scope) > 0 ->
match_rules(Scope, ACL);
match(Scope, _) ->
error({badarg, {scope, Scope}}).
match_rules(Scope, [{{_Priority, ScopePrefix}, Permissions} | Rest]) ->
% NOTE
% The `Scope` matches iff `ScopePrefix` is scope prefix of the `Scope`.
% An element of a scope matches corresponding element of a scope prefix
% according to the following rules:
% 1. Scope prefix element marked with resource and ID matches exactly the same
% scope element.
% 2. Scope prefix element marked with only resource matches any scope element
% marked with the same resource.
case match_scope(Scope, ScopePrefix) of
true ->
Permissions;
false ->
match_rules(Scope, Rest)
end;
match_rules(_Scope, []) ->
[].
match_scope([V | Ss], [V | Ss0]) ->
match_scope(Ss, Ss0);
match_scope([{V, _ID} | Ss], [V | Ss0]) ->
match_scope(Ss, Ss0);
match_scope(_, []) ->
true;
match_scope(_, _) ->
false.
%%
-spec decode([binary()]) ->
t().
decode(V) ->
lists:foldl(fun decode_entry/2, new(), V).
decode_entry(V, ACL) ->
case binary:split(V, <<":">>, [global]) of
[V1, V2] ->
Scope = decode_scope(V1),
Permission = decode_permission(V2),
insert_scope(Scope, Permission, ACL);
_ ->
error({badarg, {role, V}})
end.
decode_scope(V) ->
Hierarchy = get_resource_hierarchy(),
decode_scope_frags(binary:split(V, <<".">>, [global]), Hierarchy).
decode_scope_frags([V1, V2 | Vs], H) ->
{Resource, H1} = decode_scope_frag_resource(V1, V2, H),
[Resource | decode_scope_frags(Vs, H1)];
decode_scope_frags([V], H) ->
decode_scope_frags([V, <<"*">>], H);
decode_scope_frags([], _) ->
[].
decode_scope_frag_resource(V, <<"*">>, H) ->
R = decode_resource(V),
{R, delve(R, H)};
decode_scope_frag_resource(V, ID, H) ->
R = decode_resource(V),
{{R, ID}, delve(R, H)}.
decode_resource(V) ->
try binary_to_existing_atom(V, utf8) catch
error:badarg ->
error({badarg, {resource, V}})
end.
decode_permission(<<"read">>) ->
read;
decode_permission(<<"write">>) ->
write;
decode_permission(V) ->
error({badarg, {permission, V}}).
%%
-spec encode(t()) ->
[binary()].
encode(ACL) ->
lists:flatmap(fun encode_entry/1, ACL).
encode_entry({{_Priority, Scope}, Permissions}) ->
S = encode_scope(Scope),
[begin P = encode_permission(Permission), <<S/binary, ":", P/binary>> end
|| Permission <- Permissions].
encode_scope(Scope) ->
Hierarchy = get_resource_hierarchy(),
genlib_string:join($., encode_scope_frags(Scope, Hierarchy)).
encode_scope_frags([{Resource, ID} | Rest], H) ->
[encode_resource(Resource), ID | encode_scope_frags(Rest, delve(Resource, H))];
encode_scope_frags([Resource], H) ->
_ = delve(Resource, H),
[encode_resource(Resource)];
encode_scope_frags([Resource | Rest], H) ->
[encode_resource(Resource), <<"*">> | encode_scope_frags(Rest, delve(Resource, H))];
encode_scope_frags([], _) ->
[].
encode_resource(V) ->
atom_to_binary(V, utf8).
encode_permission(read) ->
<<"read">>;
encode_permission(write) ->
<<"write">>.
%%
get_resource_hierarchy() ->
wapi_auth:get_resource_hierarchy().
delve(Resource, Hierarchy) ->
case maps:find(Resource, Hierarchy) of
{ok, Sub} ->
Sub;
error ->
error({badarg, {resource, Resource}})
end.

203
apps/wapi/src/wapi_auth.erl Normal file
View File

@ -0,0 +1,203 @@
-module(wapi_auth).
-export([authorize_api_key/3]).
-export([authorize_operation/3]).
-export([issue_access_token/2]).
-export([issue_access_token/3]).
-export([get_subject_id/1]).
-export([get_claims/1]).
-export([get_claim/2]).
-export([get_claim/3]).
-export([get_consumer/1]).
-export([get_resource_hierarchy/0]).
-type context () :: wapi_authorizer_jwt:t().
-type claims () :: wapi_authorizer_jwt:claims().
-type consumer() :: client | merchant | provider.
-export_type([context /0]).
-export_type([claims /0]).
-export_type([consumer/0]).
-type operation_id() :: wapi_handler:operation_id().
-type api_key() ::
swag_wallet_server:api_key() |
swag_payres_server:api_key() |
swag_privdoc_server:api_key().
-type handler_opts() :: wapi_handler:handler_opts().
-spec authorize_api_key(operation_id(), api_key(), handler_opts()) ->
{true, context()}. %% | false.
authorize_api_key(_OperationID, _ApiKey, _Opts) ->
%% case parse_api_key(ApiKey) of
%% {ok, {Type, Credentials}} ->
%% case do_authorize_api_key(OperationID, Type, Credentials) of
%% {ok, Context} ->
%% {true, Context};
%% {error, Error} ->
%% _ = log_auth_error(OperationID, Error),
%% false
%% end;
%% {error, Error} ->
%% _ = log_auth_error(OperationID, Error),
%% false
%% end,
Subject = {<<"notimplemented">>, wapi_acl:new()},
Claims = #{},
{true, {Subject, Claims}}.
%% log_auth_error(OperationID, Error) ->
%% lager:info("API Key authorization failed for ~p due to ~p", [OperationID, Error]).
%% -spec parse_api_key(ApiKey :: api_key()) ->
%% {ok, {bearer, Credentials :: binary()}} | {error, Reason :: atom()}.
%% parse_api_key(ApiKey) ->
%% case ApiKey of
%% <<"Bearer ", Credentials/binary>> ->
%% {ok, {bearer, Credentials}};
%% _ ->
%% {error, unsupported_auth_scheme}
%% end.
%% -spec do_authorize_api_key(
%% OperationID :: operation_id(),
%% Type :: atom(),
%% Credentials :: binary()
%% ) ->
%% {ok, Context :: context()} | {error, Reason :: atom()}.
%% do_authorize_api_key(_OperationID, bearer, Token) ->
%% % NOTE
%% % We are knowingly delegating actual request authorization to the logic handler
%% % so we could gather more data to perform fine-grained access control.
%% wapi_authorizer_jwt:verify(Token).
%%
% TODO
% We need shared type here, exported somewhere in swagger app
-type request_data() :: #{atom() | binary() => term()}.
-spec authorize_operation(
OperationID :: operation_id(),
Req :: request_data(),
Auth :: wapi_authorizer_jwt:t()
) ->
ok | {error, unauthorized}.
%% TODO
authorize_operation(_OperationID, _Req, _) ->
ok.
%% 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.
%%
-type token_spec() ::
{destinations, DestinationID :: binary()}.
-spec issue_access_token(wapi_handler_utils:party_id(), token_spec()) ->
wapi_authorizer_jwt:token().
issue_access_token(PartyID, TokenSpec) ->
issue_access_token(PartyID, TokenSpec, unlimited).
-type 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().
issue_access_token(PartyID, TokenSpec, Expiration0) ->
Expiration = get_expiration(Expiration0),
{Claims, ACL} = resolve_token_spec(TokenSpec),
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()}].
-spec resolve_token_spec(token_spec()) ->
{claims(), acl()}.
resolve_token_spec({destinations, DestinationId}) ->
Claims = #{},
ACL = [
{[party, {destinations, DestinationId}], read},
{[party, {destinations, DestinationId}], write}
],
{Claims, ACL}.
-spec get_subject_id(context()) -> binary().
get_subject_id({{SubjectID, _ACL}, _}) ->
SubjectID.
-spec get_claims(context()) -> claims().
get_claims({_Subject, Claims}) ->
Claims.
-spec get_claim(binary(), context()) -> term().
get_claim(ClaimName, {_Subject, Claims}) ->
maps:get(ClaimName, Claims).
-spec get_claim(binary(), context(), term()) -> term().
get_claim(ClaimName, {_Subject, Claims}, Default) ->
maps:get(ClaimName, Claims, Default).
%%
%% TODO update for the wallet swag
%% -spec get_operation_access(operation_id(), request_data()) ->
%% [{wapi_acl:scope(), wapi_acl:permission()}].
%% get_operation_access('StoreBankCard' , _) ->
%% [{[payment_resources], write}].
-spec get_resource_hierarchy() -> #{atom() => map()}.
%% TODO add some sence in here
get_resource_hierarchy() ->
#{
party => #{
wallets => #{},
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

@ -0,0 +1,397 @@
-module(wapi_authorizer_jwt).
%%
-export([get_child_spec/1]).
-export([init/1]).
-export([store_key/2]).
% TODO
% Extend interface to support proper keystore manipulation
-export([issue/2]).
-export([verify/1]).
%%
-include_lib("jose/include/jose_jwk.hrl").
-include_lib("jose/include/jose_jwt.hrl").
-type keyname() :: term().
-type kid() :: binary().
-type key() :: #jose_jwk{}.
-type token() :: binary().
-type claims() :: #{binary() => term()}.
-type subject() :: {subject_id(), wapi_acl:t()}.
-type subject_id() :: binary().
-type t() :: {subject(), claims()}.
-type expiration() ::
{lifetime, Seconds :: pos_integer()} |
{deadline, UnixTs :: pos_integer()} |
unlimited.
-export_type([t/0]).
-export_type([subject/0]).
-export_type([claims/0]).
-export_type([token/0]).
-export_type([expiration/0]).
%%
-type options() :: #{
%% The set of keys used to sign issued tokens and verify signatures on such
%% tokens.
keyset => keyset(),
%% The name of a key used exclusively to sign any issued token.
%% If not set any token issue is destined to fail.
signee => keyname()
}.
-type keyset() :: #{
keyname() => keysource()
}.
-type keysource() ::
{pem_file, file:filename()}.
-spec get_child_spec(options()) ->
supervisor:child_spec() | no_return().
get_child_spec(Options) ->
#{
id => ?MODULE,
start => {supervisor, start_link, [?MODULE, parse_options(Options)]},
type => supervisor
}.
parse_options(Options) ->
Keyset = maps:get(keyset, Options, #{}),
_ = is_map(Keyset) orelse exit({invalid_option, keyset, Keyset}),
_ = genlib_map:foreach(
fun (K, V) ->
is_keysource(V) orelse exit({invalid_option, K, V})
end,
Keyset
),
Signee = maps:find(signee, Options),
{Keyset, Signee}.
is_keysource({pem_file, Fn}) ->
is_list(Fn) orelse is_binary(Fn);
is_keysource(_) ->
false.
%%
-spec init({keyset(), {ok, keyname()} | error}) ->
{ok, {supervisor:sup_flags(), [supervisor:child_spec()]}}.
init({Keyset, Signee}) ->
ok = create_table(),
KeyInfos = maps:map(fun ensure_store_key/2, Keyset),
ok = select_signee(Signee, KeyInfos),
{ok, {#{}, []}}.
ensure_store_key(Keyname, Source) ->
case store_key(Keyname, Source) of
{ok, KeyInfo} ->
KeyInfo;
{error, Reason} ->
_ = lager:error("Error importing key ~p: ~p", [Keyname, Reason]),
exit({import_error, Keyname, Source, Reason})
end.
select_signee({ok, Keyname}, KeyInfos) ->
case maps:find(Keyname, KeyInfos) of
{ok, #{sign := true}} ->
set_signee(Keyname);
{ok, KeyInfo} ->
_ = lager:error("Error setting signee: signing with ~p is not allowed", [Keyname]),
exit({invalid_signee, Keyname, KeyInfo});
error ->
_ = lager:error("Error setting signee: no key named ~p", [Keyname]),
exit({nonexstent_signee, Keyname})
end;
select_signee(error, _KeyInfos) ->
ok.
%%
-type keyinfo() :: #{
kid => kid(),
sign => boolean(),
verify => boolean()
}.
-spec store_key(keyname(), {pem_file, file:filename()}) ->
{ok, keyinfo()} | {error, file:posix() | {unknown_key, _}}.
store_key(Keyname, {pem_file, Filename}) ->
store_key(Keyname, {pem_file, Filename}, #{
kid => fun derive_kid_from_public_key_pem_entry/1
}).
derive_kid_from_public_key_pem_entry(JWK) ->
JWKPublic = jose_jwk:to_public(JWK),
{_Module, PublicKey} = JWKPublic#jose_jwk.kty,
{_PemEntry, Data, _} = public_key:pem_entry_encode('SubjectPublicKeyInfo', PublicKey),
base64url:encode(crypto:hash(sha256, Data)).
-type store_opts() :: #{
kid => fun ((key()) -> kid())
}.
-spec store_key(keyname(), {pem_file, file:filename()}, store_opts()) ->
ok | {error, file:posix() | {unknown_key, _}}.
store_key(Keyname, {pem_file, Filename}, Opts) ->
case jose_jwk:from_pem_file(Filename) of
JWK = #jose_jwk{} ->
Key = construct_key(derive_kid(JWK, Opts), JWK),
ok = insert_key(Keyname, Key),
{ok, get_key_info(Key)};
Error = {error, _} ->
Error
end.
get_key_info(#{kid := KID, signer := Signer, verifier := Verifier}) ->
#{
kid => KID,
sign => Signer /= undefined,
verify => Verifier /= undefined
}.
derive_kid(JWK, #{kid := DeriveFun}) when is_function(DeriveFun, 1) ->
DeriveFun(JWK).
construct_key(KID, JWK) ->
#{
jwk => JWK,
kid => KID,
signer => try jose_jwk:signer(JWK) catch error:_ -> undefined end,
verifier => try jose_jwk:verifier(JWK) catch error:_ -> undefined end
}.
%%
-spec issue(t(), expiration()) ->
{ok, token()} |
{error,
nonexistent_signee
}.
issue(Auth, Expiration) ->
case get_signee_key() of
Key = #{} ->
Claims = construct_final_claims(Auth, Expiration),
sign(Key, Claims);
undefined ->
{error, nonexistent_signee}
end.
construct_final_claims({{Subject, ACL}, Claims}, Expiration) ->
maps:merge(
Claims#{
<<"jti">> => unique_id(),
<<"sub">> => Subject,
<<"exp">> => get_expires_at(Expiration)
},
encode_roles(wapi_acl:encode(ACL))
).
get_expires_at({lifetime, Lt}) ->
genlib_time:unow() + Lt;
get_expires_at({deadline, Dl}) ->
Dl;
get_expires_at(unlimited) ->
0.
unique_id() ->
<<ID:64>> = snowflake:new(),
genlib_format:format_int_base(ID, 62).
sign(#{kid := KID, jwk := JWK, signer := #{} = JWS}, Claims) ->
JWT = jose_jwt:sign(JWK, JWS#{<<"kid">> => KID}, Claims),
{_Modules, Token} = jose_jws:compact(JWT),
{ok, Token}.
%%
-spec verify(token()) ->
{ok, t()} |
{error,
{invalid_token,
badarg |
{badarg, term()} |
{missing, atom()} |
expired |
{malformed_acl, term()}
} |
{nonexistent_key, kid()} |
invalid_operation |
invalid_signature
}.
verify(Token) ->
try
{_, ExpandedToken} = jose_jws:expand(Token),
#{<<"protected">> := ProtectedHeader} = ExpandedToken,
Header = wapi_utils:base64url_to_map(ProtectedHeader),
Alg = get_alg(Header),
KID = get_kid(Header),
verify(KID, Alg, ExpandedToken)
catch
%% from get_alg and get_kid
throw:Reason ->
{error, Reason};
%% TODO we're losing error information here, e.g. stacktrace
error:badarg = Reason ->
{error, {invalid_token, Reason}};
error:{badarg, _} = Reason ->
{error, {invalid_token, Reason}};
error:Reason ->
{error, {invalid_token, Reason}}
end.
verify(KID, Alg, ExpandedToken) ->
case get_key_by_kid(KID) of
#{jwk := JWK, verifier := Algs} ->
_ = lists:member(Alg, Algs) orelse throw(invalid_operation),
verify(JWK, ExpandedToken);
undefined ->
{error, {nonexistent_key, KID}}
end.
verify(JWK, ExpandedToken) ->
case jose_jwt:verify(JWK, ExpandedToken) of
{true, #jose_jwt{fields = Claims}, _JWS} ->
{#{subject_id := SubjectID}, Claims1} = validate_claims(Claims),
get_result(SubjectID, decode_roles(Claims1));
{false, _JWT, _JWS} ->
{error, invalid_signature}
end.
validate_claims(Claims) ->
validate_claims(Claims, get_validators(), #{}).
validate_claims(Claims, [{Name, Claim, Validator} | Rest], Acc) ->
V = Validator(Name, maps:get(Claim, Claims, undefined)),
validate_claims(maps:without([Claim], Claims), Rest, Acc#{Name => V});
validate_claims(Claims, [], Acc) ->
{Acc, Claims}.
get_result(SubjectID, {Roles, Claims}) ->
try
Subject = {SubjectID, wapi_acl:decode(Roles)},
{ok, {Subject, Claims}}
catch
error:{badarg, _} = Reason ->
throw({invalid_token, {malformed_acl, Reason}})
end.
get_kid(#{<<"kid">> := KID}) when is_binary(KID) ->
KID;
get_kid(#{}) ->
throw({invalid_token, {missing, kid}}).
get_alg(#{<<"alg">> := Alg}) when is_binary(Alg) ->
Alg;
get_alg(#{}) ->
throw({invalid_token, {missing, alg}}).
%%
get_validators() ->
[
{token_id , <<"jti">> , fun check_presence/2},
{subject_id , <<"sub">> , fun check_presence/2},
{expires_at , <<"exp">> , fun check_expiration/2}
].
check_presence(_, V) when is_binary(V) ->
V;
check_presence(C, undefined) ->
throw({invalid_token, {missing, C}}).
check_expiration(_, Exp = 0) ->
Exp;
check_expiration(_, Exp) when is_integer(Exp) ->
case genlib_time:unow() of
Now when Exp > Now ->
Exp;
_ ->
throw({invalid_token, expired})
end;
check_expiration(C, undefined) ->
throw({invalid_token, {missing, C}});
check_expiration(C, V) ->
throw({invalid_token, {badarg, {C, V}}}).
%%
encode_roles(Roles) ->
#{
<<"resource_access">> => #{
<<"wallet-api">> => #{
<<"roles">> => Roles
}
}
}.
decode_roles(Claims = #{
<<"resource_access">> := #{
<<"wallet-api">> := #{
<<"roles">> := Roles
}
}
}) when is_list(Roles) ->
{Roles, maps:remove(<<"resource_access">>, Claims)};
decode_roles(_) ->
throw({invalid_token, {missing, acl}}).
%%
insert_key(Keyname, Key = #{kid := KID}) ->
insert_values(#{
{keyname, Keyname} => Key,
{kid, KID} => Key
}).
get_key_by_name(Keyname) ->
lookup_value({keyname, Keyname}).
get_key_by_kid(KID) ->
lookup_value({kid, KID}).
set_signee(Keyname) ->
insert_values(#{
signee => {keyname, Keyname}
}).
get_signee_key() ->
case lookup_value(signee) of
{keyname, Keyname} ->
get_key_by_name(Keyname);
undefined ->
undefined
end.
%%
-define(TABLE, ?MODULE).
create_table() ->
_ = ets:new(?TABLE, [set, public, named_table, {read_concurrency, true}]),
ok.
insert_values(Values) ->
true = ets:insert(?TABLE, maps:to_list(Values)),
ok.
lookup_value(Key) ->
case ets:lookup(?TABLE, Key) of
[{Key, Value}] ->
Value;
[] ->
undefined
end.

View File

@ -0,0 +1,35 @@
-module(wapi_cors_policy).
-behaviour(cowboy_cors_policy).
-export([policy_init/1]).
-export([allowed_origins/2]).
-export([allowed_headers/2]).
-export([allowed_methods/2]).
-spec policy_init(cowboy_req:req()) -> {ok, cowboy_req:req(), any()}.
policy_init(Req) ->
{ok, Req, undefined_state}.
-spec allowed_origins(cowboy_req:req(), any()) -> {'*', cowboy_req:req(), any()}.
allowed_origins(Req, State) ->
{'*', Req, State}.
-spec allowed_headers(cowboy_req:req(), any()) -> {[binary()], cowboy_req:req(), any()}.
allowed_headers(Req, State) ->
{[
<<"access-control-allow-headers">>,
<<"origin">>,
<<"x-requested-with">>,
<<"content-type">>,
<<"accept">>,
<<"authorization">>,
<<"x-request-id">>
], Req, State}.
-spec allowed_methods(cowboy_req:req(), any()) -> {[binary()], cowboy_req:req(), any()}.
allowed_methods(Req, State) ->
{[<<"GET">>, <<"POST">>, <<"PUT">>, <<"DELETE">>, <<"OPTIONS">>], Req, State}.

View File

@ -0,0 +1,108 @@
-module(wapi_handler).
%% API
-export([handle_request/5]).
-export([throw_result/1]).
%% Behaviour definition
-type operation_id() ::
swag_server_payres:operation_id() |
swag_server_wallet:operation_id() |
swag_server_privdoc:operation_id().
-type swagger_context() ::
swag_server_payres:request_context() |
swag_server_wallet:request_context() |
swag_server_privdoc:request_context().
-type handler_context() :: #{
woody_context := woody_context:ctx(),
swagger_context := swagger_context()
}.
-type handler_opts() ::
swag_server_wallet:handler_opts() |
swag_server_payres:handler_opts() |
swag_server_privdoc:handler_opts().
-type req_data() :: #{atom() | binary() => term()}.
-type status_code() :: 200..599.
-type headers() :: cowboy:http_headers().
-type response_data() :: map() | [map()] | undefined.
-type request_result() :: {ok | error, {status_code(), headers(), response_data()}}.
-callback process_request(operation_id(), req_data(), handler_context(), handler_opts()) ->
request_result() | no_return().
-export_type([operation_id/0]).
-export_type([swagger_context/0]).
-export_type([handler_context/0]).
-export_type([handler_opts/0]).
-export_type([req_data/0]).
-export_type([status_code/0]).
-export_type([response_data/0]).
-export_type([headers/0]).
-export_type([request_result/0]).
%% API
-define(request_result, wapi_req_result).
-spec handle_request(operation_id(), req_data(), swagger_context(), module(), handler_opts()) ->
request_result().
handle_request(OperationID, Req, SwagContext = #{auth_context := AuthContext}, Handler, Opts) ->
_ = lager:info("Processing request ~p", [OperationID]),
try
case wapi_auth:authorize_operation(OperationID, Req, AuthContext) of
ok ->
WoodyContext = create_woody_context(Req, AuthContext, Opts),
Context = create_handler_context(SwagContext, WoodyContext),
Handler:process_request(OperationID, Req, Context, Opts)
%% ToDo: return back as soon, as authorization is implemented
%% {error, _} = Error ->
%% _ = 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">>))
end
catch
throw:{?request_result, Result} ->
Result;
error:{woody_error, {Source, Class, Details}} ->
process_woody_error(Source, Class, Details)
end.
-spec throw_result(request_result()) ->
no_return().
throw_result(Res) ->
erlang:throw({?request_result, Res}).
-spec create_woody_context(req_data(), wapi_auth:context(), handler_opts()) ->
woody_context:ctx().
create_woody_context(#{'X-Request-ID' := RequestID}, AuthContext, Opts) ->
RpcID = #{trace_id := TraceID} = woody_context:new_rpc_id(genlib:to_binary(RequestID)),
ok = scoper:add_meta(#{request_id => RequestID, trace_id => TraceID}),
_ = lager:debug("Created TraceID for the request"),
woody_user_identity:put(collect_user_identity(AuthContext, Opts), woody_context:new(RpcID)).
-define(APP, wapi).
collect_user_identity(AuthContext, _Opts) ->
genlib_map:compact(#{
id => wapi_auth:get_subject_id(AuthContext),
%% TODO pass realm via Opts
realm => genlib_app:env(?APP, realm),
email => wapi_auth:get_claim(<<"email">>, AuthContext, undefined),
username => wapi_auth:get_claim(<<"name">> , AuthContext, undefined)
}).
-spec create_handler_context(swagger_context(), woody_context:ctx()) ->
handler_context().
create_handler_context(SwagContext, WoodyContext) ->
#{
woody_context => WoodyContext,
swagger_context => SwagContext
}.
process_woody_error(_Source, result_unexpected , _Details) -> wapi_handler_utils:reply_error(500);
process_woody_error(_Source, resource_unavailable, _Details) -> wapi_handler_utils:reply_error(503);
process_woody_error(_Source, result_unknown , _Details) -> wapi_handler_utils:reply_error(504).

View File

@ -0,0 +1,87 @@
-module(wapi_handler_utils).
-export([get_error_msg/1]).
-export([reply_ok/1]).
-export([reply_ok/2]).
-export([reply_ok/3]).
-export([reply_error/1]).
-export([reply_error/2]).
-export([reply_error/3]).
-export([get_party_id/1]).
-export([get_auth_context/1]).
-export([get_location/3]).
-define(APP, wapi).
-type handler_context() :: wapi_handler:handler_context().
-type handler_opts() :: wapi_handler:handler_opts().
-type error_message() :: binary() | io_lib:chars().
-type status_code() :: wapi_handler:status_code().
-type headers() :: wapi_handler:headers().
-type response_data() :: wapi_handler:response_data().
-type party_id() :: binary().
-export_type([party_id/0]).
%% API
-spec get_party_id(handler_context()) ->
party_id().
get_party_id(Context) ->
wapi_auth:get_subject_id(get_auth_context(Context)).
-spec get_auth_context(handler_context()) ->
wapi_auth:context().
get_auth_context(#{swagger_context := #{auth_context := AuthContext}}) ->
AuthContext.
-spec get_error_msg(error_message()) ->
response_data().
get_error_msg(Message) ->
#{<<"message">> => genlib:to_binary(Message)}.
-spec reply_ok(status_code()) ->
{ok, {status_code(), [], undefined}}.
reply_ok(Code) ->
reply_ok(Code, undefined).
-spec reply_ok(status_code(), response_data()) ->
{ok, {status_code(), [], response_data()}}.
reply_ok(Code, Data) ->
reply_ok(Code, Data, []).
-spec reply_ok(status_code(), response_data(), headers()) ->
{ok, {status_code(), [], response_data()}}.
reply_ok(Code, Data, Headers) ->
reply(ok, Code, Data, Headers).
-spec reply_error(status_code()) ->
{error, {status_code(), [], undefined}}.
reply_error(Code) ->
reply_error(Code, undefined).
-spec reply_error(status_code(), response_data()) ->
{error, {status_code(), [], response_data()}}.
reply_error(Code, Data) ->
reply_error(Code, Data, []).
-spec reply_error(status_code(), response_data(), headers()) ->
{error, {status_code(), [], response_data()}}.
reply_error(Code, Data, Headers) ->
reply(error, Code, Data, Headers).
reply(Status, Code, Data, Headers) ->
{Status, {Code, Headers, Data}}.
-spec get_location(cowboy_router:route_match(), [binary()], handler_opts()) ->
headers().
get_location(PathSpec, Params, _Opts) ->
%% TODO pass base URL via Opts
BaseUrl = genlib_app:env(?APP, public_endpoint),
[{<<"Location">>, wapi_utils:get_url(BaseUrl, PathSpec, Params)}].

View File

@ -0,0 +1,142 @@
-module(wapi_payres_handler).
-include_lib("dmsl/include/dmsl_cds_thrift.hrl").
-behaviour(swag_server_payres_logic_handler).
-behaviour(wapi_handler).
%% swag_server_payres_logic_handler callbacks
-export([authorize_api_key/3]).
-export([handle_request/4]).
%% wapi_handler callbacks
-export([process_request/4]).
%% Types
-type req_data() :: wapi_handler:req_data().
-type handler_context() :: wapi_handler:handler_context().
-type request_result() :: wapi_handler:request_result().
-type operation_id() :: swag_server_payres:operation_id().
-type api_key() :: swag_server_payres:api_key().
-type request_context() :: swag_server_payres:request_context().
-type handler_opts() :: swag_server_payres:handler_opts().
%% API
-spec authorize_api_key(operation_id(), api_key(), handler_opts()) ->
false | {true, wapi_auth:context()}.
authorize_api_key(OperationID, ApiKey, Opts) ->
ok = scoper:add_meta(#{api => payres, operation_id => OperationID}),
wapi_auth:authorize_api_key(OperationID, ApiKey, Opts).
-spec handle_request(operation_id(), req_data(), request_context(), handler_opts()) ->
request_result().
handle_request(OperationID, Req, SwagContext, Opts) ->
wapi_handler:handle_request(OperationID, Req, SwagContext, ?MODULE, Opts).
-spec process_request(operation_id(), req_data(), handler_context(), handler_opts()) ->
request_result().
process_request('StoreBankCard', Req, Context, _Opts) ->
{CardData, AuthData} = process_card_data(Req, Context),
wapi_handler_utils:reply_ok(201, maps:merge(to_swag(CardData), to_swag(AuthData)));
process_request('GetBankCard', #{'token' := Token}, _Context, _Opts) ->
case decode_token(Token) of
{ok, Data} ->
wapi_handler_utils:reply_ok(200, Data);
{error, badarg} ->
wapi_handler_utils:reply_ok(404)
end.
%% Internal functions
process_card_data(#{'BankCard' := Data}, Context) ->
put_card_data_to_cds(to_thrift(card_data, Data), to_thrift(session_data, Data), Context).
put_card_data_to_cds(CardData, SessionData, Context) ->
Call = {cds_storage, 'PutCardData', [CardData, SessionData]},
case service_call(Call, Context) of
{ok, #'PutCardDataResult'{session_id = SessionID, bank_card = BankCard}} ->
{{bank_card, BankCard}, {auth_data, SessionID}};
{exception, Exception} ->
case Exception of
#'InvalidCardData'{} ->
wapi_handler:throw_result(wapi_handler_utils:reply_ok(400,
wapi_handler_utils:get_error_msg(<<"Card data is invalid">>)
));
#'KeyringLocked'{} ->
% TODO
% It's better for the cds to signal woody-level unavailability when the
% keyring is locked, isn't it? It could always mention keyring lock as a
% reason in a woody error definition.
wapi_handler:throw_result(wapi_handler_utils:reply_error(503))
end
end.
to_thrift(card_data, Data) ->
{Month, Year} = parse_exp_date(genlib_map:get(<<"expDate">>, Data)),
CardNumber = genlib:to_binary(genlib_map:get(<<"cardNumber">>, Data)),
#'CardData'{
pan = CardNumber,
exp_date = #'ExpDate'{
month = Month,
year = Year
},
cardholder_name = genlib_map:get(<<"cardHolder">>, Data, undefined),
cvv = genlib_map:get(<<"cvv">>, Data, undefined)
};
to_thrift(session_data, Data) ->
#'SessionData'{
auth_data = {card_security_code, #'CardSecurityCode'{
value = maps:get(<<"cvv">>, Data, <<>>)
}}
}.
to_swag({Spec, Data}) when is_atom(Spec) ->
to_swag(Spec, Data).
to_swag(bank_card, #domain_BankCard{
'token' = Token,
'payment_system' = PaymentSystem,
'bin' = Bin,
'masked_pan' = MaskedPan
}) ->
BankCard = genlib_map:compact(#{
<<"token">> => Token,
<<"paymentSystem">> => genlib:to_binary(PaymentSystem),
<<"bin">> => Bin,
<<"lastDigits">> => wapi_utils:get_last_pan_digits(MaskedPan)
}),
BankCard#{<<"token">> => encode_token(BankCard)};
to_swag(auth_data, PaymentSessionID) ->
#{<<"authData">> => genlib:to_binary(PaymentSessionID)}.
encode_token(TokenData) ->
wapi_utils:map_to_base64url(TokenData).
decode_token(Token) ->
try wapi_utils:base64url_to_map(Token) of
Data = #{<<"token">> := _} ->
{ok, maps:with([<<"token">>, <<"paymentSystem">>, <<"bin">>, <<"lastDigits">>],
Data#{<<"token">> => Token})
};
_ ->
{error, badarg}
catch
error:badarg ->
{error, badarg}
end.
parse_exp_date(ExpDate) when is_binary(ExpDate) ->
[Month, Year0] = binary:split(ExpDate, <<"/">>),
Year = case genlib:to_int(Year0) of
Y when Y < 100 ->
2000 + Y;
Y ->
Y
end,
{genlib:to_int(Month), Year}.
service_call({ServiceName, Function, Args}, #{woody_context := WoodyContext}) ->
wapi_woody_client:call_service(ServiceName, Function, Args, WoodyContext).

View File

@ -0,0 +1,151 @@
-module(wapi_privdoc_handler).
-include_lib("identdocstore_proto/include/identdocstore_identity_document_storage_thrift.hrl").
-behaviour(swag_server_privdoc_logic_handler).
-behaviour(wapi_handler).
%% swag_server_privdoc_logic_handler callbacks
-export([authorize_api_key/3]).
-export([handle_request/4]).
%% wapi_handler callbacks
-export([process_request/4]).
%% helper
%% TODO move it somewhere else
-export([get_proof/2]).
%% Types
-type req_data() :: wapi_handler:req_data().
-type handler_context() :: wapi_handler:handler_context().
-type request_result() :: wapi_handler:request_result().
-type operation_id() :: swag_server_privdoc:operation_id().
-type api_key() :: swag_server_privdoc:api_key().
-type request_context() :: swag_server_privdoc:request_context().
-type handler_opts() :: swag_server_privdoc:handler_opts().
%% API
-spec authorize_api_key(operation_id(), api_key(), handler_opts()) ->
false | {true, wapi_auth:context()}.
authorize_api_key(OperationID, ApiKey, Opts) ->
ok = scoper:add_meta(#{api => privdoc, operation_id => OperationID}),
wapi_auth:authorize_api_key(OperationID, ApiKey, Opts).
-spec handle_request(operation_id(), req_data(), request_context(), handler_opts()) ->
request_result().
handle_request(OperationID, Params, SwagContext, Opts) ->
wapi_handler:handle_request(OperationID, Params, SwagContext, ?MODULE, Opts).
-spec process_request(operation_id(), req_data(), handler_context(), handler_opts()) ->
request_result().
process_request('StorePrivateDocument', #{'PrivateDocument' := Params}, Context, _Opts) ->
wapi_handler_utils:reply_ok(201, process_doc_data(Params, Context)).
process_doc_data(Params, Context) ->
{ok, Token} = put_doc_data_to_cds(to_thrift(doc_data, Params), Context),
to_swag(doc, {Params, Token}).
-spec get_proof(binary(), handler_context()) -> map().
get_proof(Token, Context) ->
{ok, DocData} = service_call({identdoc_storage, 'Get', [Token]}, Context),
to_swag(doc_data, {DocData, Token}).
to_thrift(doc_data, Params = #{<<"type">> := <<"RUSDomesticPassportData">>}) ->
{russian_domestic_passport, #'identdocstore_RussianDomesticPassport'{
series = maps:get(<<"series">>, Params),
number = maps:get(<<"number">>, Params),
issuer = maps:get(<<"issuer">>, Params),
issuer_code = maps:get(<<"issuerCode">>, Params),
issued_at = maps:get(<<"issuedAt">>, Params),
family_name = maps:get(<<"familyName">>, Params),
first_name = maps:get(<<"firstName">>, Params),
patronymic = maps:get(<<"patronymic">>, Params, undefined),
birth_date = maps:get(<<"birthDate">>, Params),
birth_place = maps:get(<<"birthPlace">>, Params)
}};
to_thrift(doc_data, Params = #{<<"type">> := <<"RUSRetireeInsuranceCertificateData">>}) ->
{russian_retiree_insurance_certificate, #'identdocstore_RussianRetireeInsuranceCertificate'{
number = maps:get(<<"number">>, Params)
}}.
to_swag(doc, {Params, Token}) ->
Doc = to_swag(raw_doc, {Params, Token}),
Doc#{<<"token">> => wapi_utils:map_to_base64url(Doc)};
to_swag(raw_doc, {Params = #{<<"type">> := <<"RUSDomesticPassportData">>}, Token}) ->
#{
<<"type">> => <<"RUSDomesticPassport">>,
<<"token">> => Token,
<<"seriesMasked">> => mask(pass_series, Params),
<<"numberMasked">> => mask(pass_number, Params),
<<"fullnameMasked">> => mask(pass_fullname, Params)
};
to_swag(raw_doc, {Params = #{<<"type">> := <<"RUSRetireeInsuranceCertificateData">>}, Token}) ->
#{
<<"type">> => <<"RUSRetireeInsuranceCertificate">>,
<<"token">> => Token,
<<"numberMasked">> => mask(retiree_insurance_cert_number, Params)
};
to_swag(doc_data, {{russian_domestic_passport, D}, Token}) ->
to_swag(doc, {
#{
<<"type">> => <<"RUSDomesticPassportData">>,
<<"series">> => D#'identdocstore_RussianDomesticPassport'.series,
<<"number">> => D#'identdocstore_RussianDomesticPassport'.number,
<<"firstName">> => D#'identdocstore_RussianDomesticPassport'.first_name,
<<"familyName">> => D#'identdocstore_RussianDomesticPassport'.family_name,
<<"patronymic">> => D#'identdocstore_RussianDomesticPassport'.patronymic
},
Token
});
to_swag(doc_data, {{russian_retiree_insurance_certificate, D}, Token}) ->
to_swag(doc, {
#{
<<"type">> => <<"RUSRetireeInsuranceCertificateData">>,
<<"number">> => D#'identdocstore_RussianRetireeInsuranceCertificate'.number
},
Token
}).
put_doc_data_to_cds(IdentityDoc, Context) ->
service_call({identdoc_storage, 'Put', [IdentityDoc]}, Context).
service_call({ServiceName, Function, Args}, #{woody_context := WoodyContext}) ->
wapi_woody_client:call_service(ServiceName, Function, Args, WoodyContext).
-define(PATTERN_DIGIT, [<<"0">>, <<"1">>, <<"2">>, <<"3">>, <<"4">>, <<"5">>, <<"6">>, <<"7">>, <<"8">>, <<"9">>]).
mask(pass_series, #{<<"series">> := V}) ->
wapi_utils:mask_and_keep(leading, 2, $*, V);
mask(pass_number, #{<<"number">> := V}) ->
wapi_utils:mask_and_keep(trailing, 1, $*, V);
mask(pass_fullname, Params) ->
MaskedFamilyName = mask(family_name, Params),
MaskedFirstName = mask(first_name, Params),
MaskedPatronymic = mask(patronymic, Params),
<<MaskedFamilyName/binary, " ", MaskedFirstName/binary, MaskedPatronymic/binary>>;
mask(family_name, #{<<"familyName">> := V}) ->
wapi_utils:mask_and_keep(leading, 1, $*, V);
mask(first_name, #{<<"firstName">> := V}) ->
<<(unicode:characters_to_binary(string:left(unicode:characters_to_list(V), 1)))/binary, "."/utf8>>;
mask(patronymic, #{<<"patronymic">> := V}) ->
<<(unicode:characters_to_binary(string:left(unicode:characters_to_list(V), 1)))/binary, "."/utf8>>;
mask(patronymic, _) ->
<<>>;
%% TODO rewrite this ugly shit
mask(retiree_insurance_cert_number, #{<<"number">> := Number}) ->
FirstPublicSymbols = 2,
LastPublicSymbols = 1,
V1 = binary:part(Number, {0 , FirstPublicSymbols}),
Rest1 = binary:part(Number, {0 + FirstPublicSymbols, size(Number) - (0 + FirstPublicSymbols)}),
V2 = binary:part(Rest1, {size(Rest1) , -LastPublicSymbols}),
Rest2 = binary:part(Rest1, {0, size(Rest1) - LastPublicSymbols}),
Mask = binary:replace(Rest2, ?PATTERN_DIGIT, <<"*">>, [global]),
<<V1/binary, Mask/binary, V2/binary>>.

View File

@ -0,0 +1,54 @@
%% @doc Top level supervisor.
%% @end
-module(wapi_sup).
-behaviour(supervisor).
%% API
-export([start_link/0]).
%% Supervisor callbacks
-export([init/1]).
%%
-spec start_link() -> {ok, pid()} | {error, {already_started, pid()}}.
start_link() ->
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
%%
-spec init([]) -> {ok, {supervisor:sup_flags(), [supervisor:child_spec()]}}.
init([]) ->
AuthorizerSpecs = get_authorizer_child_specs(),
{LogicHandlers, LogicHandlerSpecs} = get_logic_handler_info(),
HealthRoutes = [{'_', [erl_health_handle:get_route(genlib_app:env(wapi, health_checkers, []))]}],
SwaggerSpec = wapi_swagger_server:child_spec({HealthRoutes, LogicHandlers}),
{ok, {
{one_for_all, 0, 1},
AuthorizerSpecs ++ LogicHandlerSpecs ++ [SwaggerSpec]
}}.
-spec get_authorizer_child_specs() -> [supervisor:child_spec()].
get_authorizer_child_specs() ->
Authorizers = genlib_app:env(wapi, authorizers, #{}),
[
get_authorizer_child_spec(jwt, maps:get(jwt, Authorizers))
].
-spec get_authorizer_child_spec(Name :: atom(), Options :: #{}) -> supervisor:child_spec().
get_authorizer_child_spec(jwt, Options) ->
wapi_authorizer_jwt:get_child_spec(Options).
-spec get_logic_handler_info() -> {Handlers :: #{atom() => module()}, [Spec :: supervisor:child_spec()] | []} .
get_logic_handler_info() ->
{#{
wallet => wapi_wallet_handler,
payres => wapi_payres_handler,
privdoc => wapi_privdoc_handler
}, []}.

View File

@ -0,0 +1,140 @@
-module(wapi_swagger_server).
-export([child_spec /1]).
-export([request_hook /1]).
-export([response_hook/4]).
-define(APP, wapi).
-define(DEFAULT_ACCEPTORS_POOLSIZE, 100).
-define(DEFAULT_IP_ADDR, "::").
-define(DEFAULT_PORT, 8080).
-define(SWAG_HANDLER_SCOPE, swag_handler).
-type params() :: {cowboy_router:routes(), #{atom() => module()}}.
-spec child_spec(params()) ->
supervisor:child_spec().
child_spec({HealthRoutes, LogicHandlers}) ->
{Transport, TransportOpts} = get_socket_transport(),
CowboyOpts = get_cowboy_config(HealthRoutes, LogicHandlers),
AcceptorsPool = genlib_app:env(?APP, acceptors_poolsize, ?DEFAULT_ACCEPTORS_POOLSIZE),
ranch:child_spec(?MODULE, AcceptorsPool,
Transport, TransportOpts, cowboy_protocol, CowboyOpts).
get_socket_transport() ->
{ok, IP} = inet:parse_address(genlib_app:env(?APP, ip, ?DEFAULT_IP_ADDR)),
Port = genlib_app:env(?APP, port, ?DEFAULT_PORT),
{ranch_tcp, [{ip, IP}, {port, Port}]}.
get_cowboy_config(HealthRoutes, LogicHandlers) ->
Dispatch =
cowboy_router:compile(squash_routes(
HealthRoutes ++
swag_server_wallet_router:get_paths(maps:get(wallet, LogicHandlers)) ++
swag_server_payres_router:get_paths(maps:get(payres, LogicHandlers)) ++
swag_server_privdoc_router:get_paths(maps:get(privdoc, LogicHandlers))
)),
[
{env, [
{dispatch, Dispatch},
{cors_policy, wapi_cors_policy}
]},
{middlewares, [
cowboy_router,
cowboy_cors,
cowboy_handler
]},
{onrequest, fun ?MODULE:request_hook/1},
{onresponse, fun ?MODULE:response_hook/4}
].
squash_routes(Routes) ->
orddict:to_list(lists:foldl(
fun ({K, V}, D) -> orddict:update(K, fun (V0) -> V0 ++ V end, V, D) end,
orddict:new(),
Routes
)).
-spec request_hook(cowboy_req:req()) ->
cowboy_req:req().
request_hook(Req) ->
ok = scoper:add_scope(?SWAG_HANDLER_SCOPE),
HookFun = cowboy_access_log:get_request_hook(),
HookFun(Req).
-spec response_hook(cowboy:http_status(), cowboy:http_headers(), iodata(), cowboy_req:req()) ->
cowboy_req:req().
response_hook(Code, Headers, Body, Req) ->
try
{Code1, Headers1, Req1} = handle_response(Code, Headers, Req),
_ = log_access(Code1, Headers1, Body, Req1),
ok = cleanup_scoper(),
Req1
catch
Class:Reason ->
Stack = genlib_format:format_stacktrace(erlang:get_stacktrace(), [newlines]),
_ = lager:warning(
"Response hook failed for: [~p, ~p, ~p]~nwith: ~p:~p~nstacktrace: ~ts",
[Code, Headers, Req, Class, Reason, Stack]
),
ok = cleanup_scoper(),
Req
end.
handle_response(Code, Headers, Req) when Code >= 500 ->
send_oops_resp(Code, Headers, get_oops_body_safe(Code), Req);
handle_response(Code, Headers, Req) ->
{Code, Headers, Req}.
%% cowboy_req:reply/4 has a faulty spec in case of response body fun.
-dialyzer({[no_contracts, no_fail_call], send_oops_resp/4}).
send_oops_resp(Code, Headers, undefined, Req) ->
{Code, Headers, Req};
send_oops_resp(Code, Headers, File, Req) ->
FileSize = filelib:file_size(File),
F = fun(Socket, Transport) ->
case Transport:sendfile(Socket, File) of
{ok, _} ->
ok;
{error, Error} ->
_ = lager:warning("Failed to send oops body: ~p", [Error]),
ok
end
end,
Headers1 = lists:foldl(
fun({K, V}, Acc) -> lists:keystore(K, 1, Acc, {K, V}) end,
Headers,
[
{<<"content-type">>, <<"text/plain; charset=utf-8">>},
{<<"content-length">>, integer_to_list(FileSize)}
]
),
{ok, Req1} = cowboy_req:reply(Code, Headers1, {FileSize, F}, Req),
{Code, Headers1, Req1}.
get_oops_body_safe(Code) ->
try get_oops_body(Code)
catch
Error:Reason ->
_ = lager:warning("Invalid oops body config for code: ~p. Error: ~p:~p", [Code, Error, Reason]),
undefined
end.
get_oops_body(Code) ->
genlib_map:get(Code, genlib_app:env(?APP, oops_bodies, #{}), undefined).
log_access(Code, Headers, Body, Req) ->
LogFun = cowboy_access_log:get_response_hook(wapi_access_lager_event),
LogFun(Code, Headers, Body, Req).
cleanup_scoper() ->
try scoper:get_current_scope() of
?SWAG_HANDLER_SCOPE -> scoper:remove_scope();
_ -> ok
catch
error:no_scopes -> ok
end.

View File

@ -0,0 +1,222 @@
-module(wapi_utils).
-export([base64url_to_map/1]).
-export([map_to_base64url/1]).
-export([to_universal_time/1]).
-export([redact/2]).
-export([mask_and_keep/4]).
-export([mask/4]).
-export([unwrap/1]).
-export([define/2]).
-export([get_path/2]).
-export([get_url/2]).
-export([get_url/3]).
-export([get_last_pan_digits/1]).
-type binding_value() :: binary().
-type url() :: binary().
-type path() :: binary().
%% API
-spec base64url_to_map(binary()) -> map() | no_return().
base64url_to_map(Base64) when is_binary(Base64) ->
try jsx:decode(base64url:decode(Base64), [return_maps])
catch
Class:Reason ->
_ = lager:debug("decoding base64 ~p to map failed with ~p:~p", [Base64, Class, Reason]),
erlang:error(badarg)
end.
-spec map_to_base64url(map()) -> binary() | no_return().
map_to_base64url(Map) when is_map(Map) ->
try base64url:encode(jsx:encode(Map))
catch
Class:Reason ->
_ = lager:debug("encoding map ~p to base64 failed with ~p:~p", [Map, Class, Reason]),
erlang:error(badarg)
end.
-spec redact(Subject :: binary(), Pattern :: binary()) -> Redacted :: binary().
redact(Subject, Pattern) ->
case re:run(Subject, Pattern, [global, {capture, all_but_first, index}]) of
{match, Captures} ->
lists:foldl(fun redact_match/2, Subject, Captures);
nomatch ->
Subject
end.
redact_match({S, Len}, Subject) ->
<<Pre:S/binary, _:Len/binary, Rest/binary>> = Subject,
<<Pre/binary, (binary:copy(<<"*">>, Len))/binary, Rest/binary>>;
redact_match([Capture], Message) ->
redact_match(Capture, Message).
%% TODO Switch to this sexy code after the upgrade to Erlang 20+
%%
%% -spec mask(leading|trailing, non_neg_integer(), char(), binary()) ->
%% binary().
%% mask(Dir = trailing, MaskLen, MaskChar, Str) ->
%% mask(Dir, 0, string:length(Str) - MaskLen, MaskChar, Str);
%% mask(Dir = leading, MaskLen, MaskChar, Str) ->
%% mask(Dir, MaskLen, string:length(Str), MaskChar, Str).
%% mask(Dir, KeepStart, KeepLen, MaskChar, Str) ->
%% 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()) ->
binary().
mask_and_keep(trailing, KeepLen, MaskChar, Chardata) ->
StrLen = erlang:length(unicode:characters_to_list(Chardata)),
mask(leading, StrLen - KeepLen, MaskChar, Chardata);
mask_and_keep(leading, KeepLen, MaskChar, Chardata) ->
StrLen = erlang:length(unicode:characters_to_list(Chardata)),
mask(trailing, StrLen - KeepLen, MaskChar, Chardata).
-spec mask(leading|trailing, non_neg_integer(), char(), binary()) ->
binary().
mask(trailing, MaskLen, MaskChar, Chardata) ->
Str = unicode:characters_to_list(Chardata),
unicode:characters_to_binary(
string:left(string:substr(Str, 1, erlang:length(Str) - MaskLen), erlang:length(Str), MaskChar)
);
mask(leading, MaskLen, MaskChar, Chardata) ->
Str = unicode:characters_to_list(Chardata),
unicode:characters_to_binary(
string:right(string:substr(Str, MaskLen + 1), erlang:length(Str), MaskChar)
).
-spec to_universal_time(Timestamp :: binary()) -> TimestampUTC :: binary().
to_universal_time(Timestamp) ->
{ok, {Date, Time, Usec, TZOffset}} = rfc3339:parse(Timestamp),
Seconds = calendar:datetime_to_gregorian_seconds({Date, Time}),
%% The following crappy code is a dialyzer workaround
%% for the wrong rfc3339:parse/1 spec.
{DateUTC, TimeUTC} = calendar:gregorian_seconds_to_datetime(
case TZOffset of
_ when is_integer(TZOffset) ->
Seconds - (60 * TZOffset);
_ ->
Seconds
end
),
{ok, TimestampUTC} = rfc3339:format({DateUTC, TimeUTC, Usec, 0}),
TimestampUTC.
-spec unwrap(ok | {ok, Value} | {error, _Error}) ->
Value | no_return().
unwrap(ok) ->
ok;
unwrap({ok, Value}) ->
Value;
unwrap({error, Error}) ->
erlang:error({unwrap_error, Error}).
-spec define(undefined | T, T) -> T.
define(undefined, V) ->
V;
define(V, _Default) ->
V.
-spec get_path(cowboy_router:route_match(), [binding_value()]) ->
path().
get_path(PathSpec, Params) when is_list(PathSpec) ->
get_path(genlib:to_binary(PathSpec), Params);
get_path(Path, []) ->
Path;
get_path(PathSpec, [Value | Rest]) ->
[P1, P2] = split(PathSpec),
P3 = get_next(P2),
get_path(<<P1/binary, Value/binary, P3/binary>>, Rest).
split(PathSpec) ->
case binary:split(PathSpec, <<":">>) of
Res = [_, _] -> Res;
[_] -> erlang:error(param_mismatch)
end.
get_next(PathSpec) ->
case binary:split(PathSpec, <<"/">>) of
[_, Next] -> <<"/", Next/binary>>;
[_] -> <<>>
end.
-spec get_url(url(), path()) ->
url().
get_url(BaseUrl, Path) ->
<<BaseUrl/binary, Path/binary>>.
-spec get_url(url(), cowboy_router:route_match(), [binding_value()]) ->
url().
get_url(BaseUrl, PathSpec, Params) ->
get_url(BaseUrl, get_path(PathSpec, Params)).
-define(MASKED_PAN_MAX_LENGTH, 4).
-spec get_last_pan_digits(binary()) ->
binary().
get_last_pan_digits(MaskedPan) when byte_size(MaskedPan) > ?MASKED_PAN_MAX_LENGTH ->
binary:part(MaskedPan, {byte_size(MaskedPan), -?MASKED_PAN_MAX_LENGTH});
get_last_pan_digits(MaskedPan) ->
MaskedPan.
%%
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
-spec test() -> _.
-spec to_universal_time_test() -> _.
to_universal_time_test() ->
?assertEqual(<<"2017-04-19T13:56:07Z">>, to_universal_time(<<"2017-04-19T13:56:07Z">>)),
?assertEqual(<<"2017-04-19T13:56:07.530000Z">>, to_universal_time(<<"2017-04-19T13:56:07.53Z">>)),
?assertEqual(<<"2017-04-19T10:36:07.530000Z">>, to_universal_time(<<"2017-04-19T13:56:07.53+03:20">>)),
?assertEqual(<<"2017-04-19T17:16:07.530000Z">>, to_universal_time(<<"2017-04-19T13:56:07.53-03:20">>)).
-spec redact_test() -> _.
redact_test() ->
P1 = <<"^\\+\\d(\\d{1,10}?)\\d{2,4}$">>,
?assertEqual(<<"+7******3210">>, redact(<<"+79876543210">>, P1)),
?assertEqual( <<"+1*11">>, redact(<<"+1111">>, P1)).
-spec get_path_test() -> _.
get_path_test() ->
?assertEqual(<<"/wallet/v0/deposits/11/events/42">>, get_path(
<<"/wallet/v0/deposits/:depositID/events/:eventID">>, [<<"11">>, <<"42">>]
)),
?assertEqual(<<"/wallet/v0/deposits/11/events/42">>, get_path(
"/wallet/v0/deposits/:depositID/events/:eventID", [<<"11">>, <<"42">>]
)),
?assertError(param_mismatch, get_path(
"/wallet/v0/deposits/:depositID/events/:eventID", [<<"11">>, <<"42">>, <<"0">>]
)).
-spec mask_test() -> _.
mask_test() ->
?assertEqual(<<"Хуй">>, mask(leading, 0, $*, <<"Хуй">>)),
?assertEqual(<<"*уй">>, mask(leading, 1, $*, <<"Хуй">>)),
?assertEqual(<<"**й">>, mask(leading, 2, $*, <<"Хуй">>)),
?assertEqual(<<"***">>, mask(leading, 3, $*, <<"Хуй">>)),
?assertEqual(<<"Хуй">>, mask(trailing, 0, $*, <<"Хуй">>)),
?assertEqual(<<"Ху*">>, mask(trailing, 1, $*, <<"Хуй">>)),
?assertEqual(<<"Х**">>, mask(trailing, 2, $*, <<"Хуй">>)),
?assertEqual(<<"***">>, mask(trailing, 3, $*, <<"Хуй">>)).
-spec mask_and_keep_test() -> _.
mask_and_keep_test() ->
?assertEqual(<<"***">>, mask_and_keep(leading, 0, $*, <<"Хуй">>)),
?assertEqual(<<"Х**">>, mask_and_keep(leading, 1, $*, <<"Хуй">>)),
?assertEqual(<<"Ху*">>, mask_and_keep(leading, 2, $*, <<"Хуй">>)),
?assertEqual(<<"Хуй">>, mask_and_keep(leading, 3, $*, <<"Хуй">>)),
?assertEqual(<<"***">>, mask_and_keep(trailing, 0, $*, <<"Хуй">>)),
?assertEqual(<<"**й">>, mask_and_keep(trailing, 1, $*, <<"Хуй">>)),
?assertEqual(<<"*уй">>, mask_and_keep(trailing, 2, $*, <<"Хуй">>)),
?assertEqual(<<"Хуй">>, mask_and_keep(trailing, 3, $*, <<"Хуй">>)).
-endif.

View File

@ -0,0 +1,466 @@
%% Temporary stab for wallet handler
-module(wapi_wallet_ff_backend).
-include_lib("dmsl/include/dmsl_domain_thrift.hrl").
%% API
-export([get_providers/2]).
-export([get_provider/2]).
-export([get_provider_identity_classes/2]).
-export([get_provider_identity_class/3]).
-export([get_provider_identity_class_levels/3]).
-export([get_provider_identity_class_level/4]).
-export([get_identities/2]).
-export([get_identity/2]).
-export([create_identity/2]).
-export([get_identity_challengies/2]).
-export([create_identity_challenge/3]).
-export([get_identity_challenge/3]).
-export([cancel_identity_challenge/3]).
-export([get_identity_challenge_events/2]).
-export([get_identity_challenge_event/4]).
-export([get_destinations/2]).
-export([get_destination/2]).
-export([create_destination/2]).
-export([create_withdrawal/2]).
-export([get_withdrawal/2]).
-export([get_withdrawal_events/2]).
-export([get_withdrawal_event/3]).
%% Helper API
-export([not_implemented/0]).
%% API
%% Providers
-spec get_providers(_, _) -> no_return().
get_providers(_Params, _Context) ->
not_implemented().
-spec get_provider(_, _) -> _.
get_provider(ProviderId, _Context) ->
case ff_provider:get(ProviderId) of
{ok, Provider} -> {ok, to_swag(provider, {ProviderId, ff_provider:payinst(Provider)})};
Error = {error, _} -> Error
end.
-spec get_provider_identity_classes(_, _) -> _.
get_provider_identity_classes(Id, _Context) ->
case ff_provider:get(Id) of
{ok, Provider} ->
{ok, lists:map(
fun(ClassId) ->
{ok, Class} = do_get_provider_identity_class(ClassId, Provider),
Class
end,
ff_provider:list_identity_classes(Provider)
)};
Error = {error, _} ->
Error
end.
-spec get_provider_identity_class(_, _, _) -> _.
get_provider_identity_class(ProviderId, ClassId, _Context) ->
case ff_provider:get(ProviderId) of
{ok, Provider} -> do_get_provider_identity_class(ClassId, Provider);
Error = {error, _} -> Error
end.
do_get_provider_identity_class(ClassId, Provider) ->
case ff_provider:get_identity_class(ClassId, Provider) of
{ok, Class} -> {ok, to_swag(identity_class, Class)};
Error = {error, _} -> Error
end.
-spec get_provider_identity_class_levels(_, _, _) -> no_return().
get_provider_identity_class_levels(_ProviderId, _ClassId, _Context) ->
not_implemented().
-spec get_provider_identity_class_level(_, _, _, _) -> no_return().
get_provider_identity_class_level(_ProviderId, _ClassId, _LevelId, _Context) ->
not_implemented().
%% Identities
-spec get_identities(_, _) -> no_return().
get_identities(_Params, _Context) ->
not_implemented().
-define(NS, <<"com.rbkmoney.wapi">>).
-spec get_identity(_, _) -> _.
get_identity(IdentityId, _Context) ->
case ff_identity_machine:get(IdentityId) of
{ok, IdentityState} ->
{ok, to_swag(identity, IdentityState)};
Error = {error, _} ->
Error
end.
-spec create_identity(_, _) -> _.
create_identity(Params, Context) ->
IdentityId = genlib:unique(),
case ff_identity_machine:create(IdentityId, from_swag(identity_params, Params), make_ctx(Params, [<<"name">>])) of
ok -> get_identity(IdentityId, Context);
{error, exists} -> create_identity(Params, Context);
Error = {error, _} -> Error
end.
-spec get_identity_challengies(_, _) -> no_return().
get_identity_challengies(_Params, _Context) ->
not_implemented().
-spec create_identity_challenge(_, _, _) -> _.
create_identity_challenge(IdentityId, Params, Context) ->
ChallengeId = genlib:unique(),
case ff_identity_machine:start_challenge(
IdentityId,
maps:merge(#{id => ChallengeId}, from_swag(identity_challenge_params, Params))
) of
ok ->
get_identity_challenge(IdentityId, ChallengeId, Context);
{error, notfound} ->
{error, {identity, notfound}};
Error = {error, _} ->
Error
end.
-spec get_identity_challenge(_, _, _) -> _.
get_identity_challenge(IdentityId, ChallengeId, Context) ->
case get_identity(IdentityId, Context) of
{ok, IdentityState} ->
case ff_identity:challenge(ChallengeId, ff_identity_machine:identity(IdentityState)) of
{ok, Challenge} ->
Proofs = [
wapi_privdoc_handler:get_proof(Token, Context) ||
{_, Token} <- ff_identity_challenge:proofs(Challenge)
],
{ok, to_swag(identity_challenge, {ChallengeId, Challenge, Proofs})};
Error = {error, notfound} ->
Error
end;
Error = {error, notfound} ->
Error
end.
-spec cancel_identity_challenge(_, _, _) -> no_return().
cancel_identity_challenge(_IdentityId, _ChallengeId, _Context) ->
not_implemented().
-spec get_identity_challenge_events(_, _) -> no_return().
get_identity_challenge_events(Params = #{'identityID' := _IdentityId, 'challengeID' := _ChallengeId, 'limit' := _Limit}, _Context) ->
_ = genlib_map:get('eventCursor', Params),
not_implemented().
-spec get_identity_challenge_event(_, _, _, _) -> no_return().
get_identity_challenge_event(_IdentityId, _ChallengeId, _EventId, _Context) ->
not_implemented().
%% Withdrawals
-spec get_destinations(_, _) -> no_return().
get_destinations(_Params, _Context) ->
not_implemented().
-spec get_destination(_, _) -> _.
get_destination(DestinationId, _Context) ->
case ff_destination_machine:get(DestinationId) of
{ok, DestinationState} -> {ok, to_swag(destination, DestinationState)};
Error = {error, _} -> Error
end.
-spec create_destination(_, _) -> _.
create_destination(Params, Context) ->
DestinationId = genlib:unique(),
case ff_destination_machine:create(
DestinationId, from_swag(destination_params, Params), make_ctx(Params, [<<"name">>])
) of
ok -> get_destination(DestinationId, Context);
{error, exists} -> create_destination(Params, Context);
Error = {error, _} -> Error
end.
-spec create_withdrawal(_, _) -> _.
create_withdrawal(Params, Context) ->
WithdrawalId = genlib:unique(),
case ff_withdrawal_machine:create(WithdrawalId, from_swag(withdrawal_params, Params), make_ctx(Params, [])) of
ok -> get_withdrawal(WithdrawalId, Context);
{error, exists} -> create_withdrawal(Params, Context);
Error = {error, _} -> Error
end.
-spec get_withdrawal(_, _) -> _.
get_withdrawal(WithdrawalId, _Context) ->
case ff_withdrawal_machine:get(WithdrawalId) of
{ok, State} -> {ok, to_swag(withdrawal, State)};
Error = {error, _} -> Error
end.
-spec get_withdrawal_events(_, _) -> _.
get_withdrawal_events(Params = #{'withdrawalID' := WithdrawalId, 'limit' := Limit}, _Context) ->
case ff_withdrawal_machine:get_status_events(WithdrawalId, genlib_map:get('eventCursor', Params)) of
{ok, Events} -> {ok, to_swag(withdrawal_events, filter_status_events(Events, Limit))};
Error = {error, _} -> Error
end.
-spec get_withdrawal_event(_, _, _) -> _.
get_withdrawal_event(WithdrawalId, EventId, _Context) ->
case ff_withdrawal_machine:get_status_events(WithdrawalId, undefined) of
{ok, Events} ->
case lists:keyfind(EventId, 1, filter_status_events(Events)) of
false -> {error, {event, notfound}};
Event -> {ok, to_swag(withdrawal_event, Event)}
end;
Error = {error, _} -> Error
end.
%% Helper API
-spec not_implemented() -> no_return().
not_implemented() ->
wapi_handler:throw_result(wapi_handler_utils:reply_error(501)).
%% Internal functions
make_ctx(Params, WapiKeys) ->
Ctx0 = maps:with(WapiKeys, Params),
Ctx1 = case maps:get(<<"metadata">>, Params, undefined) of
undefined -> Ctx0;
MD -> Ctx0#{<<"md">> => MD}
end,
#{?NS => Ctx1}.
filter_status_events(Events) ->
filter_status_events(Events, undefined).
filter_status_events(Events, Limit) ->
filter_status_events(Events, [], Limit).
filter_status_events(_, Acc, Limit) when is_integer(Limit) andalso length(Acc) >= Limit ->
Acc;
filter_status_events([], Acc, _) ->
Acc;
filter_status_events([{ID, Ts, {created, _}} | Rest], Acc, Limit) ->
filter_status_events(Rest, [{ID, Ts, undefined} | Acc], Limit);
filter_status_events([{ID, Ts, {status_changed, Status}} | Rest], Acc, Limit) ->
filter_status_events(Rest, [{ID, Ts, Status} | Acc], Limit);
filter_status_events([_ | Rest], Acc, Limit) ->
filter_status_events(Rest, Acc, Limit).
%% Marshalling
from_swag(identity_params, Params) ->
#{
party => maps:get(<<"party">> , Params),
provider => maps:get(<<"provider">>, Params),
class => maps:get(<<"class">> , Params)
};
from_swag(identity_challenge_params, Params) ->
#{
class => maps:get(<<"type">>, Params),
proofs => from_swag(proofs, maps:get(<<"proofs">>, Params))
};
from_swag(proofs, Proofs) ->
from_swag(list, {proof, Proofs});
from_swag(proof, #{<<"token">> := WapiToken}) ->
try
#{<<"type">> := Type, <<"token">> := Token} = wapi_utils:base64url_to_map(WapiToken),
{from_swag(proof_type, Type), Token}
catch
error:badarg ->
wapi_handler:throw_result(wapi_handler_utils:reply_error(
422,
wapi_handler_utils:get_error_msg(io_lib:format("Invalid proof token: ~p", [WapiToken]))
))
end;
from_swag(proof_type, <<"RUSDomesticPassport">>) ->
rus_domestic_passport;
from_swag(proof_type, <<"RUSRetireeInsuranceCertificateData">>) ->
rus_retiree_insurance_cert;
from_swag(destination_params, Params) ->
#{
identity => maps:get(<<"identity">>, Params),
currency => maps:get(<<"currency">>, Params),
name => maps:get(<<"name">> , Params),
resource => from_swag(destination_resource, maps:get(<<"resource">>, Params))
};
from_swag(destination_resource, #{
<<"type">> := <<"BankCardDestinationResource">>,
<<"token">> := WapiToken
}) ->
#{<<"token">> := CdsToken} = wapi_utils:base64url_to_map(WapiToken),
{bank_card, #{token => CdsToken}};
from_swag(withdrawal_params, Params) ->
#{
source => maps:get(<<"wallet">> , Params),
destination => maps:get(<<"destination">>, Params),
body => from_swag(withdrawal_body , maps:get(<<"body">>, Params))
};
from_swag(withdrawal_body, Body) ->
{maps:get(<<"amount">>, Body), maps:get(<<"currency">>, Body)};
from_swag(list, {Type, List}) ->
lists:map(fun(V) -> from_swag(Type, V) end, List).
to_swag(_, undefined) ->
undefined;
to_swag(providers, Providers) ->
to_swag(list, {provider, Providers});
to_swag(provider, {Id, Provider}) ->
to_swag(map, #{
<<"id">> => Id,
<<"name">> => Provider#'domain_PaymentInstitution'.name,
<<"residences">> => to_swag(list, {residence,
ordsets:to_list(Provider#'domain_PaymentInstitution'.residences)
})
});
to_swag(residence, Residence) ->
genlib_string:to_upper(genlib:to_binary(Residence));
to_swag(identity_class, Class) ->
to_swag(map, maps:with([id, name], Class));
to_swag(identity, #{identity := Identity, times := {CreatedAt, _}, ctx := Ctx}) ->
{ok, WapiCtx} = ff_ctx:get(?NS, Ctx),
ProviderId = ff_provider:id(ff_identity:provider(Identity)),
#{id := ClassId} = ff_identity:class(Identity),
to_swag(map, #{
<<"id">> => ff_identity:id(Identity),
<<"name">> => maps:get(<<"name">>, WapiCtx),
<<"metadata">> => maps:get(<<"md">>, WapiCtx, undefined),
<<"createdAt">> => to_swag(timestamp, CreatedAt),
<<"provider">> => ProviderId,
<<"class">> => ClassId,
<<"level">> => ff_identity_class:level_id(ff_identity:level(Identity)),
<<"effectiveChallenge">> => to_swag(identity_effective_challenge, ff_identity:effective_challenge(Identity)),
<<"isBlocked">> => to_swag(is_blocked, ff_identity:is_accessible(Identity))
});
to_swag(identity_effective_challenge, {ok, ChallegeId}) ->
ChallegeId;
to_swag(identity_effective_challenge, {error, notfound}) ->
undefined;
to_swag(identity_challenge, {ChallengeId, Challenge, Proofs}) ->
ChallengeClass = ff_identity_challenge:class(Challenge),
to_swag(map,#{
<<"id">> => ChallengeId,
<<"createdAt">> => <<"TODO">>,
<<"level">> => ff_identity_class:level_id(ff_identity_class:target_level(ChallengeClass)),
<<"type">> => ff_identity_class:challenge_class_id(ChallengeClass),
<<"proofs">> => Proofs,
<<"status">> => to_swag(challenge_status,
{ff_identity_challenge:status(Challenge), ff_identity_challenge:resolution(Challenge)}
),
<<"validUntil">> => to_swag(idenification_expiration, ff_identity_challenge:status(Challenge)),
<<"failureReason">> => to_swag(identity_challenge_failure_reason, ff_identity_challenge:status(Challenge))
});
to_swag(challenge_status, {pending, _}) ->
<<"Pending">>;
to_swag(challenge_status, {completed, {ok, approved}}) ->
<<"Completed">>;
to_swag(challenge_status, {completed, {ok, denied}}) ->
<<"Failed">>;
to_swag(challenge_status, {failed, _}) ->
<<"Failed">>;
to_swag(challenge_status, cancelled) ->
<<"Cancelled">>;
to_swag(idenification_expiration, {completed, #{resolution := approved, valid_until := Timestamp}}) ->
to_swag(timestamp, Timestamp);
to_swag(idenification_expiration, _) ->
undefined;
to_swag(identity_challenge_failure_reason, {completed, #{resolution := denied}}) ->
<<"Denied">>;
to_swag(identity_challenge_failure_reason, {failed, Reason}) ->
genlib:to_binary(Reason);
to_swag(identity_challenge_failure_reason, _) ->
undefined;
to_swag(destination, #{destination := Destination, times := {CreatedAt, _}, ctx := Ctx}) ->
{ok, WapiCtx} = ff_ctx:get(?NS, Ctx),
Wallet = ff_destination:wallet(Destination),
to_swag(map, #{
<<"id">> => ff_destination:id(Destination),
<<"name">> => maps:get(<<"name">>, WapiCtx),
<<"metadata">> => maps:get(<<"md">>, WapiCtx, undefined),
<<"createdAt">> => to_swag(timestamp, CreatedAt),
%% TODO
<<"isBlocked">> => to_swag(is_blocked, {ok, accessible}), %% ff_destination:is_accessible(Destination)),
<<"identity">> => ff_identity:id(ff_wallet:identity(Wallet)),
<<"currency">> => to_swag(currency, ff_wallet:currency(Wallet)),
<<"resource">> => to_swag(destination_resource, ff_destination:resource(Destination)),
<<"status">> => to_swag(destination_status, ff_destination:status(Destination)),
<<"validUntil">> => to_swag(destination_expiration, Destination)
});
to_swag(destination_status, authorized) ->
<<"Authorized">>;
to_swag(destination_status, unauthorized) ->
<<"Unauthorized">>;
to_swag(destination_expiration, #{status := authorized, timeout := Timeout}) ->
Timeout;
to_swag(destination_expiration, _) ->
undefined;
to_swag(destination_resource, {bank_card, BankCard}) ->
to_swag(map, #{
<<"type">> => <<"BankCardDestinationResource">>,
<<"token">> => maps:get(token, BankCard),
<<"paymentSystem">> => genlib_map:get(payment_system, BankCard),
<<"bin">> => genlib_map:get(bin, BankCard),
<<"lastDigits">> => to_swag(pan_last_digits, genlib_map:get(masked_pan, BankCard))
});
to_swag(pan_last_digits, MaskedPan) ->
wapi_utils:get_last_pan_digits(MaskedPan);
to_swag(withdrawal, St = #{withdrawal := W, times := {CreatedAt, _}, ctx := Ctx}) ->
{ok, WapiCtx} = ff_ctx:get(?NS, Ctx),
Status = genlib_map:get(status, St),
to_swag(map, #{
<<"id">> => ff_withdrawal:id(W),
<<"createdAt">> => to_swag(timestamp, CreatedAt),
<<"metadata">> => maps:get(<<"md">>, WapiCtx, undefined),
<<"wallet">> => ff_wallet:id(ff_withdrawal:source(W)),
<<"destination">> => ff_destination:id(ff_withdrawal:destination(W)),
<<"body">> => to_swag(withdrawal_body, ff_withdrawal:body(W)),
<<"status">> => to_swag(withdrawal_status, Status),
<<"failure">> => to_swag(withdrawal_failure, Status)
});
to_swag(withdrawal_body, Body) ->
to_swag(map, #{
<<"amount">> => maps:get(amount, Body),
<<"currency">> => to_swag(currency, maps:get(currency, Body))
});
to_swag(withdrawal_status, succeeded) ->
<<"Succeeded">>;
to_swag(withdrawal_status, failed) ->
<<"Failed">>;
to_swag(withdrawal_status, {failed, _Reason}) ->
<<"Failed">>;
to_swag(withdrawal_status, _) ->
<<"Pending">>;
to_swag(withdrawal_failure, {failed, Reason}) ->
genlib:to_binary(Reason);
to_swag(withdrawal_failure, _) ->
undefined;
to_swag(withdrawal_events, Events) ->
to_swag(list, {withdrawal_event, Events});
to_swag(withdrawal_event, {EventId, Ts, Status}) ->
to_swag(map, #{
<<"eventID">> => EventId,
<<"occuredAt">> => to_swag(timestamp, Ts),
<<"changes">> => [#{
<<"type">> => <<"WithdrawalStatusChanged">>,
<<"status">> => to_swag(withdrawal_status, Status),
<<"failure">> => to_swag(withdrawal_failure, Status)
}]
});
to_swag(timestamp, {{Date, Time}, Usec}) ->
rfc3339:format({Date, Time, Usec, undefined});
to_swag(currency, Currency) ->
genlib_string:to_upper(genlib:to_binary(Currency));
to_swag(is_blocked, {ok, accessible}) ->
false;
to_swag(is_blocked, _) ->
true;
to_swag(_Type, V) when is_map(V) ->
to_swag(map, V);
to_swag(list, {Type, List}) ->
lists:map(fun(V) -> to_swag(Type, V) end, List);
to_swag(map, Map) ->
genlib_map:compact(Map).

View File

@ -0,0 +1,267 @@
-module(wapi_wallet_handler).
-behaviour(swag_server_wallet_logic_handler).
-behaviour(wapi_handler).
%% swag_server_wallet_logic_handler callbacks
-export([authorize_api_key/3]).
-export([handle_request/4]).
%% wapi_handler callbacks
-export([process_request/4]).
%% Types
-type req_data() :: wapi_handler:req_data().
-type handler_context() :: wapi_handler:handler_context().
-type request_result() :: wapi_handler:request_result().
-type operation_id() :: swag_server_wallet:operation_id().
-type api_key() :: swag_server_wallet:api_key().
-type request_context() :: swag_server_wallet:request_context().
-type handler_opts() :: swag_server_wallet:handler_opts().
%% API
-spec authorize_api_key(operation_id(), api_key(), handler_opts()) ->
false | {true, wapi_auth:context()}.
authorize_api_key(OperationID, ApiKey, Opts) ->
ok = scoper:add_meta(#{api => wallet, operation_id => OperationID}),
wapi_auth:authorize_api_key(OperationID, ApiKey, Opts).
-spec handle_request(swag_server_wallet:operation_id(), req_data(), request_context(), handler_opts()) ->
request_result().
handle_request(OperationID, Req, SwagContext, Opts) ->
wapi_handler:handle_request(OperationID, Req, SwagContext, ?MODULE, Opts).
%% Providers
-spec process_request(operation_id(), req_data(), handler_context(), handler_opts()) ->
request_result().
process_request('ListProviders', Req, #{woody_context := Context}, _Opts) ->
case wapi_wallet_ff_backend:get_providers(maps:with(['residence'], Req), Context) of
{ok, Providers} -> wapi_handler_utils:reply_ok(200, Providers);
{error, notfound} -> wapi_handler_utils:reply_ok(200, [])
end;
process_request('GetProvider', #{'providerID' := Id}, #{woody_context := Context}, _Opts) ->
case wapi_wallet_ff_backend:get_provider(Id, Context) of
{ok, Provider} -> wapi_handler_utils:reply_ok(200, Provider);
{error, notfound} -> wapi_handler_utils:reply_ok(404)
end;
process_request('ListProviderIdentityClasses', #{'providerID' := Id}, #{woody_context := Context}, _Opts) ->
case wapi_wallet_ff_backend:get_provider_identity_classes(Id, Context) of
{ok, Classes} -> wapi_handler_utils:reply_ok(200, Classes);
{error, notfound} -> wapi_handler_utils:reply_ok(404)
end;
process_request('GetProviderIdentityClass', #{
'providerID' := ProviderId,
'identityClassID' := ClassId
}, #{woody_context := Context}, _Opts) ->
case wapi_wallet_ff_backend:get_provider_identity_class(ProviderId, ClassId, Context) of
{ok, Class} -> wapi_handler_utils:reply_ok(200, Class);
{error, notfound} -> wapi_handler_utils:reply_ok(404)
end;
process_request('ListProviderIdentityLevels', #{
'providerID' := ProviderId,
'identityClassID' := ClassId
}, #{woody_context := Context}, _Opts) ->
case wapi_wallet_ff_backend:get_provider_identity_class_levels(ProviderId, ClassId, Context) of
{ok, Levels} -> wapi_handler_utils:reply_ok(200, Levels);
{error, notfound} -> wapi_handler_utils:reply_ok(404)
end;
process_request('GetProviderIdentityLevel', #{
'providerID' := ProviderId,
'identityClassID' := ClassId,
'identityLevelID' := LevelId
}, #{woody_context := Context}, _Opts) ->
case wapi_wallet_ff_backend:get_provider_identity_class_level(ProviderId, ClassId, LevelId, Context) of
{ok, Level} -> wapi_handler_utils:reply_ok(200, Level);
{error, notfound} -> wapi_handler_utils:reply_ok(404)
end;
%% Identities
process_request('ListIdentities', Req, #{woody_context := Context}, _Opts) ->
case wapi_wallet_ff_backend:get_identities(maps:with(['provider', 'class', 'level'], Req), Context) of
{ok, Identities} -> wapi_handler_utils:reply_ok(200, Identities);
{error, notfound} -> wapi_handler_utils:reply_ok(404)
end;
process_request('GetIdentity', #{'identityID' := IdentityId}, #{woody_context := Context}, _Opts) ->
case wapi_wallet_ff_backend:get_identity(IdentityId, Context) of
{ok, Identity} -> wapi_handler_utils:reply_ok(200, Identity);
{error, notfound} -> wapi_handler_utils:reply_ok(404)
end;
process_request(O = 'CreateIdentity', #{'Identity' := Params}, C = #{woody_context := Context}, Opts) ->
case wapi_wallet_ff_backend:create_identity(Params#{<<"party">> => wapi_handler_utils:get_party_id(C)}, Context) of
{ok, Identity = #{<<"id">> := IdentityId}} ->
wapi_handler_utils:reply_ok(201, Identity, get_location(O, [IdentityId], Opts));
{error, {provider, notfound}} ->
wapi_handler_utils:reply_ok(422, wapi_handler_utils:get_error_msg(<<"No such provider">>));
{error, {identity_class, notfound}} ->
wapi_handler_utils:reply_ok(422, wapi_handler_utils:get_error_msg(<<"No such identity class">>))
end;
process_request('ListIdentityChallenges', Req, #{woody_context := Context}, _Opts) ->
case wapi_wallet_ff_backend:get_identity_challengies(maps:with(['status', 'identityID'], Req), Context) of
{ok, Challengies} -> wapi_handler_utils:reply_ok(200, Challengies);
{error, notfound} -> wapi_handler_utils:reply_ok(404)
end;
process_request(O = 'StartIdentityChallenge', #{
'identityID' := IdentityId,
'IdentityChallenge' := Params
}, #{woody_context := Context}, Opts) ->
case wapi_wallet_ff_backend:create_identity_challenge(IdentityId, Params, Context) of
{ok, Challenge = #{<<"id">> := ChallengeId}} ->
wapi_handler_utils:reply_ok(202, Challenge, get_location(O, [ChallengeId], Opts));
{error, {identity, notfound}} ->
wapi_handler_utils:reply_ok(404);
{error, {challenge, {pending, _}}} ->
wapi_handler_utils:reply_ok(409);
{error, {challenge, {class, notfound}}} ->
wapi_handler_utils:reply_ok(422, wapi_handler_utils:get_error_msg(<<"No such identity class">>));
{error, {challenge, {proof, notfound}}} ->
wapi_handler_utils:reply_ok(422, wapi_handler_utils:get_error_msg(<<"Proof not found">>));
{error, {challenge, {proof, insufficient}}} ->
wapi_handler_utils:reply_ok(422, wapi_handler_utils:get_error_msg(<<"Insufficient proof">>))
%% TODO any other possible errors here?
end;
process_request('GetIdentityChallenge', #{
'identityID' := IdentityId,
'challengeID' := ChallengeId
}, #{woody_context := Context}, _Opts) ->
case wapi_wallet_ff_backend:get_identity_challenge(IdentityId, ChallengeId, Context) of
{ok, Challenge} -> wapi_handler_utils:reply_ok(200, Challenge);
{error, notfound} -> wapi_handler_utils:reply_ok(404)
end;
process_request('CancelIdentityChallenge', #{
'identityID' := IdentityId,
'challengeID' := ChallengeId
}, #{woody_context := Context}, _Opts) ->
wapi_wallet_ff_backend:cancel_identity_challenge(IdentityId, ChallengeId, Context);
process_request('PollIdentityChallengeEvents', Params, #{woody_context := Context}, _Opts) ->
case wapi_wallet_ff_backend:get_identity_challenge_events(Params, Context) of
{ok, Events} -> wapi_handler_utils:reply_ok(200, Events);
{error, notfound} -> wapi_handler_utils:reply_ok(404)
end;
process_request('GetIdentityChallengeEvent', #{
'identityID' := IdentityId,
'challengeID' := ChallengeId,
'eventID' := EventId
}, #{woody_context := Context}, _Opts) ->
case wapi_wallet_ff_backend:get_identity_challenge_event(IdentityId, ChallengeId, EventId, Context) of
{ok, Event} -> wapi_handler_utils:reply_ok(200, Event);
{error, notfound} -> wapi_handler_utils:reply_ok(404);
{error, {event, notfound}} -> wapi_handler_utils:reply_ok(404)
end;
%% Wallets
process_request(O, _Req, _Context, _Opts) when
O =:= 'ListWallets' orelse
O =:= 'CreateWallet' orelse
O =:= 'GetWallet' orelse
O =:= 'GetWalletAccount' orelse
O =:= 'IssueWalletGrant'
->
wapi_wallet_ff_backend:not_implemented();
%% Deposits
process_request(O, _Req, _Context, _Opts) when
O =:= 'CreateDeposit' orelse
O =:= 'GetDeposit' orelse
O =:= 'PollDepositEvents' orelse
O =:= 'GetDepositEvents'
->
wapi_wallet_ff_backend:not_implemented();
%% Withdrawals
process_request('ListDestinations', Req, #{woody_context := Context}, _Opts) ->
case wapi_wallet_ff_backend:get_destinations(maps:with(['identity', 'currency'], Req), Context) of
{ok, Destinations} -> wapi_handler_utils:reply_ok(200, Destinations);
{error, notfound} -> wapi_handler_utils:reply_ok(200, [])
end;
process_request('GetDestination', #{'destinationID' := DestinationId}, #{woody_context := Context}, _Opts) ->
case wapi_wallet_ff_backend:get_destination(DestinationId, Context) of
{ok, Destination} -> wapi_handler_utils:reply_ok(200, Destination);
{error, notfound} -> wapi_handler_utils:reply_ok(404)
end;
process_request(O = 'CreateDestination', #{'Destination' := Params}, C = #{woody_context := Context}, Opts) ->
case wapi_wallet_ff_backend:create_destination(Params#{party => wapi_handler_utils:get_party_id(C)}, Context) of
{ok, Destination = #{<<"id">> := DestinationId}} ->
wapi_handler_utils:reply_ok(201, Destination, get_location(O, [DestinationId], Opts));
{error, {identity, notfound}} ->
wapi_handler_utils:reply_ok(422, wapi_handler_utils:get_error_msg(<<"No such identity">>));
{error, {currency, notfound}} ->
wapi_handler_utils:reply_ok(422, wapi_handler_utils:get_error_msg(<<"Currency not supported">>))
end;
process_request('IssueDestinationGrant', #{
'destinationID' := DestinationId,
'DestinationGrantRequest' := #{<<"validUntil">> := Expiration}
}, #{woody_context := Context}, _Opts) ->
ExpirationUTC = wapi_utils:to_universal_time(Expiration),
ok = check_expiration(ExpirationUTC),
case wapi_wallet_ff_backend:get_destination(DestinationId, Context) of
{ok, _Destination} ->
{ok, {Date, Time, Usec, _Tz}} = rfc3339:parse(ExpirationUTC),
wapi_handler_utils:reply_ok(201, #{
<<"token">> => wapi_auth:issue_access_token(
wapi_handler_utils:get_party_id(Context),
{destinations, DestinationId},
{deadline, {{Date, Time}, Usec}}
),
<<"validUntil">> => Expiration
});
{error, notfound} ->
wapi_handler_utils:reply_ok(404)
end;
process_request(O = 'CreateWithdrawal', #{'WithdrawalParameters' := Params}, #{woody_context := Context}, Opts) ->
%% TODO: check authorization crap here (or on the backend)
case wapi_wallet_ff_backend:create_withdrawal(Params, Context) of
{ok, Withdrawal = #{<<"id">> := WithdrawalId}} ->
wapi_handler_utils:reply_ok(201, Withdrawal, get_location(O, [WithdrawalId], Opts));
{error, {source, notfound}} ->
wapi_handler_utils:reply_ok(422, wapi_handler_utils:get_error_msg(<<"No such wallet">>));
{error, {destination, notfound}} ->
wapi_handler_utils:reply_ok(422, wapi_handler_utils:get_error_msg(<<"No such wallet">>))
end;
process_request('GetWithdrawal', #{'withdrawalID' := WithdrawalId}, #{woody_context := Context}, _Opts) ->
case wapi_wallet_ff_backend:get_withdrawal(WithdrawalId, Context) of
{ok, Withdrawal} -> wapi_handler_utils:reply_ok(200, Withdrawal);
{error, notfound} -> wapi_handler_utils:reply_ok(422, wapi_handler_utils:get_error_msg(<<"No such withdrawal">>))
end;
process_request('PollWithdrawalEvents', Params, #{woody_context := Context}, _Opts) ->
case wapi_wallet_ff_backend:get_withdrawal_events(Params, Context) of
{ok, Events} -> wapi_handler_utils:reply_ok(200, Events);
{error, notfound} -> wapi_handler_utils:reply_ok(404)
end;
process_request('GetWithdrawalEvents', #{
'withdrawalID' := WithdrawalId,
'eventID' := EventId
}, #{woody_context := Context}, _Opts) ->
case wapi_wallet_ff_backend:get_withdrawal_event(WithdrawalId, EventId, Context) of
{ok, Event} -> wapi_handler_utils:reply_ok(200, Event);
{error, notfound} -> wapi_handler_utils:reply_ok(404);
{error, {event, notfound}} -> wapi_handler_utils:reply_ok(404)
end;
%% Residences
process_request('GetResidence', _Req, _Context, _Opts) ->
wapi_wallet_ff_backend:not_implemented();
%% Currencies
process_request('GetCurrency', _Req, _Context, _Opts) ->
wapi_wallet_ff_backend:not_implemented().
%% Internal functions
get_location(OperationId, Params, Opts) ->
#{path := PathSpec} = swag_server_wallet_router:get_operation(OperationId),
wapi_handler_utils:get_location(PathSpec, Params, Opts).
check_expiration(Expiration) ->
{ok, ExpirationSec} = rfc3339:to_time(Expiration, second),
case (genlib_time:unow() - ExpirationSec) >= 0 of
true ->
wapi_handler:throw_result(wapi_handler_utils:reply_ok(
422,
wapi_handler_utils:get_error_msg(<<"Already expired">>)
));
false ->
ok
end.

View File

@ -0,0 +1,227 @@
%% Temporary stab for wallet handler
-module(wapi_wallet_mock_backend).
-include_lib("dmsl/include/dmsl_domain_thrift.hrl").
-export([get_providers/2]).
-export([get_provider/2]).
-export([get_provider_identity_classes/2]).
-export([get_provider_identity_class/3]).
-export([get_provider_identity_class_levels/3]).
-export([get_provider_identity_class_level/4]).
-export([get_identities/2]).
-export([get_identity/2]).
-export([create_identity/2]).
-export([get_destinations/2]).
-export([get_destination/2]).
-export([create_destination/2]).
-export([create_withdrawal/2]).
-export([get_withdrawal/2]).
-export([get_withdrawal_events/2]).
-export([get_withdrawal_event/3]).
%% API
-spec get_providers(_, _) -> _.
get_providers(_Params, _Context) ->
{ok, [#{
<<"id">> => <<"1">>,
<<"name">> => <<"НКО «ЭПС»">>,
<<"residences">> => [<<"RUS">>]
}]}.
-spec get_provider(_, _) -> _.
get_provider(_Id, _Context) ->
{ok, #{
<<"id">> => <<"1">>,
<<"name">> => <<"НКО «ЭПС»">>,
<<"residences">> => [<<"RUS">>]
}}.
-spec get_provider_identity_classes(_, _) -> _.
get_provider_identity_classes(_Id, _Context) ->
{ok, [#{
<<"id">> => <<"person">>,
<<"name">> => <<"Частная харя">>
}]}.
-spec get_provider_identity_class(_, _, _) -> _.
get_provider_identity_class(_ProviderId, _ClassId, _Context) ->
{ok, #{id => <<"person">>, name => <<"Частная харя">>}}.
-spec get_provider_identity_class_levels(_, _, _) -> _.
get_provider_identity_class_levels(_ProviderId, _ClassId, _Context) ->
{ok, [
#{
<<"id">> => <<"partial">>,
<<"name">> => <<"Частично идентифицирован(а/о)">>,
<<"challenges">> => #{
<<"id">> => <<"esia">>,
<<"name">> => <<"Упрощённая идентификация">>,
<<"requiredProofs">> => [
<<"RUSDomesticPassport">>,
<<"RUSRetireeInsuranceCertificate">>
]
}
},
#{
<<"id">> => <<"full">>,
<<"name">> => <<"Полностью идентифицирован(а/о)">>,
<<"challenges">> => #{
<<"id">> => <<"svyaznoi bpa">>,
<<"name">> => <<"Полная идентификацияв Связном">>,
<<"requiredProofs">> => [
<<"RUSDomesticPassport">>,
<<"RUSRetireeInsuranceCertificate">>
]
}
}
]}.
-spec get_provider_identity_class_level(_, _, _, _) -> _.
get_provider_identity_class_level(_ProviderId, _ClassId, _LevelId, _Context) ->
{ok, #{
<<"id">> => <<"partial">>,
<<"name">> => <<"Частично идентифицирован(а/о)">>,
<<"challenges">> => #{
<<"id">> => <<"esia">>,
<<"name">> => <<"Упрощённая идентификация">>,
<<"requiredProofs">> => [
<<"RUSDomesticPassport">>,
<<"RUSRetireeInsuranceCertificate">>
]
}
}}.
-spec get_identities(_, _) -> _.
get_identities(_Params, _Context) ->
{ok, [#{
<<"id">> => <<"douknowdawae">>,
<<"name">> => <<"Keyn Fawkes aka Slug">>,
<<"metadata">> => #{<<"is real">> => false},
<<"createdAt">> => {{{1989, 01, 17}, {12, 01, 45}}, 0},
<<"provider">> => <<"1">>,
<<"class">> => <<"person">>,
<<"level">> => <<"partial">>,
<<"effectiveChallenge">> => <<"25">>,
<<"isBlocked">> => false
}]}.
-spec get_identity(_, _) -> _.
get_identity(IdentityId, _Context) ->
{ok, #{
<<"id">> => IdentityId,
<<"name">> => <<"Keyn Fawkes aka Slug">>,
<<"metadata">> => #{<<"is real">> => false},
<<"createdAt">> => {{{1989, 01, 17}, {12, 01, 45}}, 0},
<<"provider">> => <<"1">>,
<<"class">> => <<"person">>,
<<"level">> => <<"partial">>,
<<"effectiveChallenge">> => <<"25">>,
<<"isBlocked">> => false
}}.
-spec create_identity(_, _) -> _.
create_identity(_Params, Context) ->
get_identity(woody_context:new_req_id(), Context).
-spec get_destinations(_, _) -> _.
get_destinations(_Params, _Context) ->
{ok, [#{
<<"id">> => <<"107498">>,
<<"name">> => <<"Squarey plastic thingy">>,
<<"metadata">> => #{<<"display_name">> => <<"Картофан СБЕР">>},
<<"createdAt">> => <<"2018-06-20T08:56:02Z">>,
<<"isBlocked">> => false,
<<"identity">> => <<"douknowdawae">>,
<<"currency">> => <<"RUB">>,
<<"resource">> => get_destination_resource(what, ever),
<<"status">> => <<"Authorized">>,
<<"validUntil">> => <<"2018-06-20T08:56:02Z">>
}]}.
-spec get_destination(_, _) -> _.
get_destination(_DestinationId, _Context) ->
{ok, #{
<<"id">> => <<"107498">>,
<<"name">> => <<"Squarey plastic thingy">>,
<<"metadata">> => #{<<"display_name">> => <<"Картофан СБЕР">>},
<<"createdAt">> => <<"2018-06-20T08:56:02Z">>,
<<"isBlocked">> => false,
<<"identity">> => <<"douknowdawae">>,
<<"currency">> => <<"RUB">>,
<<"resource">> => get_destination_resource(what, ever),
<<"status">> => <<"Authorized">>,
<<"validUntil">> => <<"2018-06-20T08:56:02Z">>
}}.
-spec create_destination(_, _) -> _.
create_destination(_Params, Context) ->
get_destination(woody_context:new_req_id(), Context).
-spec get_withdrawal(_, _) -> _.
get_withdrawal(WithdrawalId, _Context) ->
{ok, #{
<<"id">> => WithdrawalId,
<<"createdAt">> => {{{2018, 06, 17}, {12, 01, 45}}, 0},
<<"wallet">> => woody_context:new_req_id(),
<<"destination">> => woody_context:new_req_id(),
<<"body">> => #{
<<"amount">> => 1430000,
<<"currency">> => <<"RUB">>
},
<<"status">> => <<"Pending">>,
<<"metadata">> => #{<<"who'sthedaddy">> => <<"me">>}
}}.
-spec create_withdrawal(_, _) -> _.
create_withdrawal(_Params, Context) ->
get_withdrawal(woody_context:new_req_id(), Context).
-spec get_withdrawal_events(_, _) -> _.
get_withdrawal_events(_, _) ->
[#{
<<"eventID">> => 1,
<<"occuredAt">> => "2018-06-28T12:49:12Z",
<<"changes">> => [#{
<<"type">> => <<"WithdrawalStatusChanged">>,
<<"status">> => <<"Pending">>
}]
},
#{
<<"eventID">> => 5,
<<"occuredAt">> => "2018-06-28T12:49:13Z",
<<"changes">> => [#{
<<"type">> => <<"WithdrawalStatusChanged">>,
<<"status">> => <<"Failed">>,
<<"failure">> => <<"tolkonepiu is not a function">>
}]
}].
-spec get_withdrawal_event(_, _, _) -> _.
get_withdrawal_event(_WithdrawalId, EventId, _) ->
#{
<<"eventID">> => EventId,
<<"occuredAt">> => "2018-07-24T04:37:45Z",
<<"changes">> => [#{
<<"type">> => <<"WithdrawalStatusChanged">>,
<<"status">> => <<"Succeeded">>
}]
}.
%% Internals
get_destination_resource(_, _) ->
#{
<<"type">> => <<"BankCardDestinationResource">>,
<<"bin">> => <<"424242">>,
<<"lastDigits">> => <<"4242">>,
<<"paymentSystem">> => <<"visa">>,
<<"token">> => <<
"eyJiaW4iOiI0MjQyNDIiLCJsYXN0RGlnaXRzIjoiNDI0MiIsInBheW1lbnRTeXN0ZW"
"0iOiJ2aXNhIiwidG9rZW4iOiI3NXlQSkZac1lCOEFvdEFUS0dFa3p6In0"
>>
}.

View File

@ -0,0 +1,41 @@
-module(wapi_woody_client).
-export([call_service/4]).
-export([call_service/5]).
-export([get_service_modname/1]).
%%
-define(APP, wapi).
-type service_name() :: atom().
-spec call_service(service_name(), woody:func(), [term()], woody_context:ctx()) ->
woody:result().
call_service(ServiceName, Function, Args, Context) ->
call_service(ServiceName, Function, Args, Context, scoper_woody_event_handler).
-spec call_service(service_name(), woody:func(), [term()], woody_context:ctx(), woody:ev_handler()) ->
woody:result().
call_service(ServiceName, Function, Args, Context, EventHandler) ->
{Url, Service} = get_service_spec(ServiceName),
Request = {Service, Function, Args},
woody_client:call(Request, #{url => Url, event_handler => EventHandler}, Context).
get_service_spec(ServiceName) ->
{get_service_url(ServiceName), get_service_modname(ServiceName)}.
get_service_url(ServiceName) ->
maps:get(ServiceName, genlib_app:env(?APP, service_urls)).
-spec get_service_modname(service_name()) -> woody:service().
get_service_modname(cds_storage) ->
{dmsl_cds_thrift, 'Storage'};
get_service_modname(identdoc_storage) ->
{identdocstore_identity_document_storage_thrift, 'IdentityDocumentStorage'}.
%% get_service_modname(webhook_manager) ->
%% {dmsl_webhooker_thrift, 'WebhookManager'}.

View File

@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA4MUtYkvoIAHNgvYtHSydanyY1qD8nJ+D/A1FFp5LF4SmM9nn
vSfTFC2T3D53sCR/DtUzCFIQwZIXXHob22ndFydZqhahrYLLJkpH5IXMy593Sho/
oXzxgwkbXaOMevcLFZcj5AneG+q2vFjaDGeQAJaAAPGinMo6UN94DYguNH2s6zqo
yRc8ng6KWD5UgEFTIEWni1RIZvp2NAnSkh/SeI1zs9uY6AR7bf6oFSChTd9m+li5
d20L5tc0aX7LG842SJEM2dJKckI4ZDZHvU6nDitH3TGrxkMa0CqLe7nUOfvSff2c
H9m0CzSbPy/SnyTQLklWoFsi9z2cqqtY6SvR7QIDAQABAoIBADAoz1KSZQgGmtwG
lx/7ITdhvvWtxLJiU0s8JKN2Ayzk1R+i/s4+rDFUmqvEDq0FBNxOvgJ4YvK2tJ6x
4yoeAqslWUbiVn3w2ko3/DNwn7K5VjvgZ+XX+X9UAjMMCduG9y8HFT+VBawBnGm6
t+2UevxFQuPw4iCqC9isKPLtTMkeBXfaCA+tzBqVytlBeW5nJG1Bh9GSV6OeeNoc
x6lh1X+7kxk/qLQZsogNwZXxPLuIK0qJCfsGzMYodSi43nv2mFtl5vBt0M+iU42i
KrL32SlQmkBI4st/HIie9YpSjj55llOU6L0KBPhH58wc8LDEc2Kwcxeow4/McO0E
fSwf9pkCgYEA+4v+371szXbUTfOBOBO7+bGbTo0gzJ8JnMaSdVDLhBaqyp5dbztS
TPiaCqfEYk4AYnl2dR7nLYRca/WRDle7hvDqB7K2RWWS58RDifiQ4gfJM9lW4Ocu
SIhnxVmr4iVdo4eOs6pxe8yRtF1U+uK8WuoV06+lgL/esEJB2JPyeQsCgYEA5L/U
osQFOogSk1Ycjl66UEXm0Y2HzFONTKMSellUnkdSSscx6+mLOn7eL5voSNSJrnCw
Tfh3uZ0NOh63Yw3aPGCwtn+EIflW1hzx+DJMvCS5TaU3BZF954rljklJL6VpaIPP
fXrc0z1FcsAT2s3aQNmEK2SWp7Y44V6mpQn7a+cCgYEA0Tf+dD+MOFRmfrNSvb6E
MUkMwMfXCPoaN6BdfmAF9cYYpdAULIjtigGXtdcWGyF/ZmhaI03hv9UAPfcQgBpu
ae0E6gQ1YAD8r/Jorl/kuWr6aTqS7Rq7Py7dCKLtuHmVqYb9JOhV3T8nzRl3rfhZ
61AZeWj1QeHUKUvikm1zVkMCgYEAyan42xn3BhgKUEw9VqJanQRTLnEYxGDwlBy7
4JM6j2OPQA+GilXVgddxKAXJ7dM6IkiElei0HDZB//gucqw2tr4DbJDUu2LnVFIm
XEpz7fZuSu6ZqFYQ6n1ATYV8eP3aBOMXnKchYTWGMVj26BJNFJju9ZZzXx293aol
PiCjwAcCgYAmOtRZRtf/p1eXPz1JN1OwEVSrnghJP5KBA8XGsnBmQUTeMmHo3Wl7
rELKg0O2bsPtTTAvm5bfLsRgvee+EY28mAY6MA8xJNHB6OabOHuRHqX7ow/LOagK
15mUtZ9f8AaKamZ3Bmg/XWWJxNmeCt5LJDr1OnmCDyItbfF9DxnXXg==
-----END RSA PRIVATE KEY-----

View File

@ -62,8 +62,33 @@
}
}},
{services, #{
'partymgmt' => "http://hellgate:8022/v1/processing/partymgmt",
'accounter' => "http://shumway:8022/accounter"
'partymgmt' => "http://hellgate:8022/v1/processing/partymgmt",
'accounter' => "http://shumway:8022/accounter",
'identification' => "http://identification:8022/v1/identification"
}}
]},
%% wapi
{wapi, [
{ip, "::"},
{port, 8080},
%% To send ASCII text in 5xx replies
%% {oops_bodies, #{
%% 500 => "oops_bodies/500_body"
%% }},
{realm, <<"external">>},
{authorizers, #{
jwt => #{
signee => wapi,
keyset => #{
wapi => {pem_file, "var/keys/wapi/private.pem"}
}
}
}},
{service_urls, #{
webhook_manager => "http://hooker:8022/hook",
cds_storage => "http://cds:8022/v1/storage",
identdoc_storage => "http://cds:8022/v1/identity_document_storage"
}}
]},
@ -78,7 +103,16 @@
}},
{services, #{
'automaton' => "http://machinegun:8022/v1/automaton"
}}
}},
{net_opts, [
% Bump keepalive timeout up to a minute
{timeout, 60000}
]},
{health_checkers, [
{erl_health, disk , ["/", 99] },
{erl_health, cg_memory, [99] },
{erl_health, service , [<<"wapi">>]}
]}
]}
].

View File

@ -116,7 +116,13 @@
{vm_args , "./config/vm.args"},
{dev_mode , false},
{include_erts , true},
{extended_start_script , true}
{extended_start_script , true},
%% wapi
{overlay, [
{mkdir , "var/keys/wapi" },
{copy , "apps/wapi/var/keys/wapi/private.pem", "var/keys/wapi/private.pem" }
]}
]}
]},

View File

@ -1,7 +1,20 @@
{"1.1.0",
[{<<"certifi">>,{pkg,<<"certifi">>,<<"0.7.0">>},2},
{<<"cowboy">>,{pkg,<<"cowboy">>,<<"1.0.4">>},1},
{<<"cowlib">>,{pkg,<<"cowlib">>,<<"1.0.2">>},2},
[{<<"base64url">>,{pkg,<<"base64url">>,<<"0.0.1">>},0},
{<<"certifi">>,{pkg,<<"certifi">>,<<"0.7.0">>},1},
{<<"cg_mon">>,
{git,"https://github.com/rbkmoney/cg_mon.git",
{ref,"5a87a37694e42b6592d3b4164ae54e0e87e24e18"}},
1},
{<<"cowboy">>,{pkg,<<"cowboy">>,<<"1.0.4">>},0},
{<<"cowboy_access_log">>,
{git,"git@github.com:rbkmoney/cowboy_access_log.git",
{ref,"99df4e8069d3d581a55d287e6bc0cb575c67d5ec"}},
0},
{<<"cowboy_cors">>,
{git,"https://github.com/danielwhite/cowboy_cors.git",
{ref,"392f5804b63fff2bd0fda67671d5b2fbe0badd37"}},
0},
{<<"cowlib">>,{pkg,<<"cowlib">>,<<"1.0.2">>},1},
{<<"dmsl">>,
{git,"git@github.com:rbkmoney/damsel.git",
{ref,"62d932bc3984301a6394aa8addade6f2892ac79f"}},
@ -14,6 +27,10 @@
{git,"git@github.com:rbkmoney/dmt_core.git",
{ref,"045c78132ecce5a8ec4a2e6ccd2c6b0b65bade1f"}},
1},
{<<"erl_health">>,
{git,"https://github.com/rbkmoney/erlang-health.git",
{ref,"ab3ca1ccab6e77905810aa270eb936dbe70e02f8"}},
0},
{<<"erlang_localtime">>,
{git,"https://github.com/kpy3/erlang_localtime",
{ref,"c79fa7dd454343e7cbbdcce0c7a95ad86af1485d"}},
@ -24,25 +41,43 @@
0},
{<<"goldrush">>,{pkg,<<"goldrush">>,<<"0.1.9">>},1},
{<<"gproc">>,{pkg,<<"gproc">>,<<"0.6.1">>},0},
{<<"hackney">>,{pkg,<<"hackney">>,<<"1.6.2">>},1},
{<<"hackney">>,{pkg,<<"hackney">>,<<"1.6.2">>},0},
{<<"id_proto">>,
{git,"git@github.com:rbkmoney/identification-proto.git",
{ref,"2da2c4717afd7fd7ef7caba3ef9013a9eb557250"}},
0},
{<<"idna">>,{pkg,<<"idna">>,<<"1.2.0">>},2},
{<<"identdocstore_proto">>,
{git,"git@github.com:rbkmoney/identdocstore-proto.git",
{ref,"ccd301e37c128810c9f68d7a64dd8183af91b2bf"}},
0},
{<<"idna">>,{pkg,<<"idna">>,<<"1.2.0">>},1},
{<<"jesse">>,
{git,"https://github.com/rbkmoney/jesse.git",
{ref,"39105922d1ce5834383d8e8aa877c60319b9834a"}},
0},
{<<"jose">>,{pkg,<<"jose">>,<<"1.7.9">>},0},
{<<"jsx">>,{pkg,<<"jsx">>,<<"2.8.2">>},0},
{<<"lager">>,{pkg,<<"lager">>,<<"3.6.1">>},0},
{<<"machinery">>,
{git,"git@github.com:rbkmoney/machinery.git",
{ref,"eb1beed9a287d8b6ab8c68b782b2143ef574c99d"}},
0},
{<<"metrics">>,{pkg,<<"metrics">>,<<"1.0.1">>},2},
{<<"metrics">>,{pkg,<<"metrics">>,<<"1.0.1">>},1},
{<<"mg_proto">>,
{git,"git@github.com:rbkmoney/machinegun_proto.git",
{ref,"5c07c579014f9900357f7a72f9d10a03008b9da1"}},
0},
{<<"mimerl">>,{pkg,<<"mimerl">>,<<"1.0.2">>},2},
{<<"mimerl">>,{pkg,<<"mimerl">>,<<"1.0.2">>},1},
{<<"parse_trans">>,
{git,"https://github.com/rbkmoney/parse_trans.git",
{ref,"5ee45f5bfa6c04329bea3281977b774f04c89f11"}},
0},
{<<"payproc_errors">>,
{git,"git@github.com:rbkmoney/payproc-errors-erlang.git",
{ref,"9c720534eb88edc6ba47af084939efabceb9b2d6"}},
0},
{<<"quickrand">>,{pkg,<<"quickrand">>,<<"1.7.3">>},1},
{<<"ranch">>,{pkg,<<"ranch">>,<<"1.5.0">>},2},
{<<"ranch">>,{pkg,<<"ranch">>,<<"1.5.0">>},1},
{<<"rfc3339">>,{pkg,<<"rfc3339">>,<<"0.2.2">>},0},
{<<"scoper">>,
{git,"git@github.com:rbkmoney/scoper.git",
@ -52,7 +87,7 @@
{git,"https://github.com/rbkmoney/snowflake.git",
{ref,"0a598108f6582affe3b4ae550fc5b9f2062e318a"}},
1},
{<<"ssl_verify_fun">>,{pkg,<<"ssl_verify_fun">>,<<"1.1.1">>},2},
{<<"ssl_verify_fun">>,{pkg,<<"ssl_verify_fun">>,<<"1.1.1">>},1},
{<<"thrift">>,
{git,"https://github.com/rbkmoney/thrift_erlang.git",
{ref,"240bbc842f6e9b90d01bd07838778cf48752b510"}},
@ -68,6 +103,7 @@
0}]}.
[
{pkg_hash,[
{<<"base64url">>, <<"36A90125F5948E3AFD7BE97662A1504B934DD5DAC78451CA6E9ABF85A10286BE">>},
{<<"certifi">>, <<"861A57F3808F7EB0C2D1802AFEAAE0FA5DE813B0DF0979153CBAFCD853ABABAF">>},
{<<"cowboy">>, <<"A324A8DF9F2316C833A470D918AAF73AE894278B8AA6226CE7A9BF699388F878">>},
{<<"cowlib">>, <<"9D769A1D062C9C3AC753096F868CA121E2730B9A377DE23DEC0F7E08B1DF84EE">>},
@ -75,6 +111,8 @@
{<<"gproc">>, <<"4579663E5677970758A05D8F65D13C3E9814EC707AD51D8DCEF7294EDA1A730C">>},
{<<"hackney">>, <<"96A0A5E7E65B7ACAD8031D231965718CC70A9B4131A8B033B7543BBD673B8210">>},
{<<"idna">>, <<"AC62EE99DA068F43C50DC69ACF700E03A62A348360126260E87F2B54ECED86B2">>},
{<<"jose">>, <<"9DC5A14AB62DB4E41677FCC97993752562FB57AD0B8BA062589682EDD3ACB91F">>},
{<<"jsx">>, <<"7ACC7D785B5ABE8A6E9ADBDE926A24E481F29956DD8B4DF49E3E4E7BCC92A018">>},
{<<"lager">>, <<"9D29C5FF7F926D25ECD9899990867C9152DCF34EEE65BAC8EC0DFC0D16A26E0C">>},
{<<"metrics">>, <<"25F094DEA2CDA98213CECC3AEFF09E940299D950904393B2A29D191C346A8486">>},
{<<"mimerl">>, <<"993F9B0E084083405ED8252B99460C4F0563E41729AB42D9074FD5E52439BE88">>},

1
schemes/swag Submodule

@ -0,0 +1 @@
Subproject commit ab892644583d093e78137feb5a65a3fdbd170c7d