Skip to content

Commit f473bb0

Browse files
authored
Update master (#17)
* APM-107: Handle tokens in digital wallets (#6) * Add valitydev/tds-proto@48bca49 dependency * Rename `api_deadlines` config option * Bump to empayre/swag-wallets@c64e4c9 * Bump to valitydev/bouncer-client-erlang@b6c7be0 # Conflicts: # apps/wapi/test/wapi_destination_tests_SUITE.erl # rebar.lock * fixed after merge * fixed
1 parent aa5d14e commit f473bb0

21 files changed

+270
-80
lines changed

Dockerfile

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,12 @@ ARG OTP_VERSION
22

33
# Build the release
44
FROM docker.io/library/erlang:${OTP_VERSION} AS builder
5-
6-
ARG BUILDARCH
5+
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
76

87
# Install thrift compiler
98
ARG THRIFT_VERSION
10-
11-
RUN wget -q -O- "https://github.com/valitydev/thrift/releases/download/${THRIFT_VERSION}/thrift-${THRIFT_VERSION}-linux-${BUILDARCH}.tar.gz" \
9+
ARG TARGETARCH
10+
RUN wget -q -O- "https://github.com/valitydev/thrift/releases/download/${THRIFT_VERSION}/thrift-${THRIFT_VERSION}-linux-${TARGETARCH}.tar.gz" \
1211
| tar -xvz -C /usr/local/bin/
1312

1413
# Copy sources
@@ -17,8 +16,8 @@ COPY . /build/
1716

1817
# Build the release
1918
WORKDIR /build
20-
RUN rebar3 compile
21-
RUN rebar3 as prod release
19+
RUN rebar3 compile && \
20+
rebar3 as prod release
2221

2322
# Make a runner image
2423
FROM docker.io/library/erlang:${OTP_VERSION}-slim
@@ -29,15 +28,15 @@ ARG SERVICE_NAME
2928
ENV CHARSET=UTF-8
3029
ENV LANG=C.UTF-8
3130

32-
# Expose SERVICE_NAME as env so CMD expands properly on start
33-
ENV SERVICE_NAME=${SERVICE_NAME}
34-
3531
# Set runtime
3632
WORKDIR /opt/${SERVICE_NAME}
3733

3834
COPY --from=builder /build/_build/prod/rel/${SERVICE_NAME} /opt/${SERVICE_NAME}
3935

36+
RUN echo "#!/bin/sh" >> /entrypoint.sh && \
37+
echo "exec /opt/${SERVICE_NAME}/bin/${SERVICE_NAME} foreground" >> /entrypoint.sh && \
38+
chmod +x /entrypoint.sh
4039
ENTRYPOINT []
41-
CMD /opt/${SERVICE_NAME}/bin/${SERVICE_NAME} foreground
40+
CMD ["/entrypoint.sh"]
4241

43-
EXPOSE 8022
42+
EXPOSE 8022

Dockerfile.dev

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,17 @@
11
ARG OTP_VERSION
22

33
FROM docker.io/library/erlang:${OTP_VERSION}
4-
5-
ARG BUILDARCH
4+
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
65

76
# Install thrift compiler
87
ARG THRIFT_VERSION
9-
10-
RUN wget -q -O- "https://github.com/valitydev/thrift/releases/download/${THRIFT_VERSION}/thrift-${THRIFT_VERSION}-linux-${BUILDARCH}.tar.gz" \
8+
ARG TARGETARCH
9+
RUN wget -q -O- "https://github.com/valitydev/thrift/releases/download/${THRIFT_VERSION}/thrift-${THRIFT_VERSION}-linux-${TARGETARCH}.tar.gz" \
1110
| tar -xvz -C /usr/local/bin/
1211

1312
# Set env
1413
ENV CHARSET=UTF-8
1514
ENV LANG=C.UTF-8
1615

1716
# Set runtime
18-
CMD /bin/bash
17+
CMD ["/bin/bash"]

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ dev-image: .image.dev
2525

2626
.image.dev: Dockerfile.dev .env
2727
$(DOCKER) build $(DOTENV:%=--build-arg %) -f Dockerfile.dev -t $(DEV_IMAGE_TAG) .
28-
$(DOCKER) image ls -q -f "reference=$(DEV_IMAGE_ID)" | head -n1 > $@
28+
$(DOCKER) image ls -q -f "reference=$(DEV_IMAGE_TAG)" | head -n1 > $@
2929

3030
clean-dev-image:
3131
ifneq ($(DEV_IMAGE_ID),)

apps/wapi/src/wapi.app.src

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@
3030
token_keeper_client,
3131
thrift,
3232
wapi_core,
33-
dmt_client
33+
dmt_client,
34+
tds_proto
3435
]},
3536
{env, []}
3637
]}.

apps/wapi/src/wapi_backend_utils.erl

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
-module(wapi_backend_utils).
22

3-
-include_lib("fistful_proto/include/ff_proto_base_thrift.hrl").
4-
-include_lib("fistful_proto/include/ff_proto_identity_thrift.hrl").
53
-include_lib("fistful_proto/include/ff_proto_wallet_thrift.hrl").
64
-include_lib("fistful_proto/include/ff_proto_destination_thrift.hrl").
75
-include_lib("fistful_proto/include/ff_proto_w2w_transfer_thrift.hrl").

apps/wapi/src/wapi_codec.erl

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
-module(wapi_codec).
22

33
-include_lib("fistful_proto/include/ff_proto_base_thrift.hrl").
4-
-include_lib("fistful_proto/include/ff_proto_repairer_thrift.hrl").
54
-include_lib("fistful_proto/include/ff_proto_account_thrift.hrl").
6-
-include_lib("fistful_proto/include/ff_proto_msgpack_thrift.hrl").
75

86
-export([unmarshal/2]).
97
-export([unmarshal/3]).

apps/wapi/src/wapi_destination_backend.erl

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@
2727
| {external_id_conflict, {id(), external_id()}}.
2828
create(Params, HandlerContext) ->
2929
do(fun() ->
30-
ResourceThrift = unwrap(construct_resource(maps:get(<<"resource">>, Params))),
30+
ResourceIn = maps:get(<<"resource">>, Params),
31+
Resource = secure_resource(ResourceIn, HandlerContext),
32+
ResourceThrift = unwrap(construct_resource(Resource)),
3133
ID = unwrap(generate_id(Params, ResourceThrift, HandlerContext)),
3234
unwrap(create_request(ID, Params, ResourceThrift, HandlerContext))
3335
end).
@@ -114,16 +116,27 @@ get_by_external_id(ExternalID, HandlerContext = #{woody_context := WoodyContext}
114116
%% Internal
115117
%%
116118

117-
construct_resource(#{
118-
<<"token">> := Token,
119-
<<"type">> := Type
120-
}) ->
119+
secure_resource(
120+
#{<<"type">> := <<"DigitalWalletDestinationResource">>, <<"token">> := Token} = Resource,
121+
#{woody_context := WoodyContext}
122+
) ->
123+
TokenID = wapi_token_storage:put(Token, WoodyContext),
124+
Resource#{<<"token">> => TokenID};
125+
secure_resource(Resource, _HandlerContext) ->
126+
Resource.
127+
128+
construct_resource(
129+
#{
130+
<<"type">> := <<"BankCardDestinationResource">> = Type,
131+
<<"token">> := Token
132+
}
133+
) ->
121134
case wapi_backend_utils:decode_resource(Token) of
122135
{ok, Resource} ->
123136
{bank_card, BankCard} = Resource,
124137
{ok, {bank_card, #'fistful_base_ResourceBankCard'{bank_card = BankCard}}};
125138
{error, Error} ->
126-
logger:warning("~p token decryption failed: ~p", [Type, Error]),
139+
_ = logger:warning("BankCardDestinationResource token decryption failed: ~p", [Error]),
127140
{error, {invalid_resource_token, Type}}
128141
end;
129142
construct_resource(
@@ -145,20 +158,19 @@ construct_resource(
145158
<<"type">> := <<"DigitalWalletDestinationResource">>,
146159
<<"id">> := DigitalWalletID,
147160
<<"provider">> := Provider
148-
}
161+
} = Resource
149162
) ->
150163
ConstructedResource =
151164
{digital_wallet, #{
152165
digital_wallet => #{
153166
id => marshal(string, DigitalWalletID),
154-
payment_service => #{id => marshal(string, Provider)}
167+
payment_service => #{id => marshal(string, Provider)},
168+
token => maybe_marshal(string, maps:get(<<"token">>, Resource, undefined))
155169
}
156170
}},
157171
{ok, wapi_codec:marshal(resource, ConstructedResource)};
158172
construct_resource(
159-
Resource = #{
160-
<<"type">> := GenericResourceType
161-
}
173+
Resource = #{<<"type">> := GenericResourceType}
162174
) ->
163175
case prepare_generic_resource_data(GenericResourceType, Resource) of
164176
{ok, Data} ->
@@ -176,6 +188,16 @@ construct_resource(
176188

177189
tokenize_resource({bank_card, #'fistful_base_ResourceBankCard'{bank_card = BankCard}}) ->
178190
wapi_backend_utils:tokenize_resource({bank_card, BankCard});
191+
tokenize_resource({digital_wallet, Resource}) ->
192+
% NOTE
193+
% Deliberately excluding `token` from hashing because at this point it contains random string
194+
% and would break conflict detection otherwise.
195+
DigitalWallet = Resource#'fistful_base_ResourceDigitalWallet'.digital_wallet,
196+
wapi_backend_utils:tokenize_resource(
197+
{digital_wallet, Resource#'fistful_base_ResourceDigitalWallet'{
198+
digital_wallet = DigitalWallet#'fistful_base_DigitalWallet'{token = undefined}
199+
}}
200+
);
179201
tokenize_resource(Value) ->
180202
wapi_backend_utils:tokenize_resource(Value).
181203

apps/wapi/src/wapi_handler.erl

Lines changed: 10 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
-module(wapi_handler).
22

33
%% API
4-
-export([handle_request/5]).
4+
-export([handle_request/4]).
55
-export([throw_result/1]).
66
-export([respond_if_forbidden/2]).
77

88
%% Behaviour definition
99

10-
-type tag() :: wallet | payres.
11-
1210
-type operation_id() :: swag_server_wallet:operation_id().
1311

1412
-type swagger_context() :: swag_server_wallet:request_context().
@@ -58,31 +56,28 @@
5856
%% API
5957

6058
-define(REQUEST_RESULT, wapi_req_result).
61-
-define(APP, wapi).
6259

63-
-spec handle_request(tag(), operation_id(), req_data(), swagger_context(), opts()) -> request_result().
64-
handle_request(Tag, OperationID, Req, SwagContext, Opts) ->
60+
-spec handle_request(operation_id(), req_data(), swagger_context(), opts()) -> request_result().
61+
handle_request(OperationID, Req, SwagContext, Opts) ->
6562
#{'X-Request-Deadline' := Header} = Req,
6663
case wapi_utils:parse_deadline(Header) of
6764
{ok, Deadline} ->
68-
WoodyContext = attach_deadline(Deadline, create_woody_context(Tag, Req)),
69-
process_request(Tag, OperationID, Req, SwagContext, Opts, WoodyContext);
65+
WoodyContext = attach_deadline(Deadline, create_woody_context(Req)),
66+
process_request(OperationID, Req, SwagContext, Opts, WoodyContext);
7067
_ ->
71-
_ = logger:warning("Operation ~p failed due to invalid deadline header ~p", [OperationID, Header]),
7268
wapi_handler_utils:reply_ok(400, #{
7369
<<"errorType">> => <<"SchemaViolated">>,
7470
<<"name">> => <<"X-Request-Deadline">>,
7571
<<"description">> => <<"Invalid data in X-Request-Deadline header">>
7672
})
7773
end.
7874

79-
process_request(Tag, OperationID, Req, SwagContext0, Opts, WoodyContext) ->
75+
process_request(OperationID, Req, SwagContext0, Opts, WoodyContext) ->
8076
_ = logger:info("Processing request ~p", [OperationID]),
8177
try
8278
SwagContext = do_authorize_api_key(SwagContext0, WoodyContext),
8379
Context = create_handler_context(OperationID, SwagContext, WoodyContext),
84-
Handler = get_handler(Tag),
85-
{ok, RequestState} = Handler:prepare(OperationID, Req, Context, Opts),
80+
{ok, RequestState} = wapi_wallet_handler:prepare(OperationID, Req, Context, Opts),
8681
#{authorize := Authorize, process := Process} = RequestState,
8782
{ok, Resolution} = Authorize(),
8883
case Resolution of
@@ -114,15 +109,11 @@ respond_if_forbidden(forbidden, Response) ->
114109
respond_if_forbidden(allowed, _Response) ->
115110
allowed.
116111

117-
get_handler(wallet) -> wapi_wallet_handler;
118-
get_handler(payres) -> wapi_payres_handler.
119-
120-
-spec create_woody_context(tag(), req_data()) -> woody_context:ctx().
121-
create_woody_context(Tag, #{'X-Request-ID' := RequestID}) ->
112+
-spec create_woody_context(req_data()) -> woody_context:ctx().
113+
create_woody_context(#{'X-Request-ID' := RequestID}) ->
122114
RpcID = #{trace_id := TraceID} = woody_context:new_rpc_id(genlib:to_binary(RequestID)),
123115
ok = scoper:add_meta(#{request_id => RequestID, trace_id => TraceID}),
124-
_ = logger:debug("Created TraceID for the request"),
125-
woody_context:new(RpcID, undefined, wapi_woody_client:get_service_deadline(Tag)).
116+
woody_context:new(RpcID, undefined, wapi_woody_client:get_service_deadline(wallet)).
126117

127118
attach_deadline(undefined, Context) ->
128119
Context;
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
-module(wapi_redact_event_handler).
2+
3+
-export([new/2]).
4+
5+
-behaviour(woody_event_handler).
6+
-export([handle_event/4]).
7+
8+
-type secret() :: binary().
9+
-type options() :: {[secret()], woody:ev_handlers()}.
10+
11+
-spec new([secret()], woody:ev_handlers()) ->
12+
woody:handler(options()).
13+
new(Secrets, Handlers) ->
14+
{?MODULE, {lists:usort(Secrets), Handlers}}.
15+
16+
-spec handle_event(Event, RpcId, Meta, Opts) -> ok when
17+
Event :: woody_event_handler:event(),
18+
RpcId :: woody:rpc_id() | undefined,
19+
Meta :: woody_event_handler:event_meta(),
20+
Opts :: options().
21+
handle_event(Event, RpcID, Meta, {Secrets, Handlers}) ->
22+
FilteredMeta = filter_meta(Meta, Secrets),
23+
woody_event_handler:handle_event(Handlers, Event, RpcID, FilteredMeta).
24+
25+
-define(REDACTED, <<"***">>).
26+
-define(FALLBACK, <<"*FALLBACK*">>).
27+
28+
filter_meta(Meta, Secrets) ->
29+
maps:map(fun(Name, Value) -> filter(Name, Value, Secrets) end, Meta).
30+
31+
filter(Meta, Value, Secrets) when
32+
Meta == args;
33+
Meta == result;
34+
Meta == reason;
35+
Meta == error;
36+
Meta == stack
37+
->
38+
filter(Value, Secrets);
39+
filter(_, Value, _Secrets) ->
40+
Value.
41+
42+
%% common
43+
filter(L, Secrets) when is_list(L) ->
44+
[filter(E, Secrets) || E <- L];
45+
filter(T, Secrets) when is_tuple(T) ->
46+
list_to_tuple(filter(tuple_to_list(T), Secrets));
47+
filter(M, Secrets) when is_map(M) ->
48+
genlib_map:truemap(fun(K, V) -> {filter(K, Secrets), filter(V, Secrets)} end, M);
49+
filter(B, Secrets) when is_binary(B) ->
50+
binary:replace(B, Secrets, ?REDACTED, [global]);
51+
filter(V, _) when is_atom(V) ->
52+
V;
53+
filter(V, _) when is_number(V) ->
54+
V;
55+
filter(P, _) when is_pid(P) ->
56+
P;
57+
filter(P, _) when is_port(P) ->
58+
P;
59+
filter(F, _) when is_function(F) ->
60+
F;
61+
filter(R, _) when is_reference(R) ->
62+
R;
63+
%% NOTE
64+
%% Fallback. Notably covers «bitstrings-but-not-binaries» which are unexpected in
65+
%% the context of Woody RPC anyway.
66+
filter(_V, _) ->
67+
?FALLBACK.

apps/wapi/src/wapi_stream_h.erl

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,11 +63,8 @@ handle_response({response, Code, Headers, Body}) when Code >= 500 ->
6363
handle_response({response, _, _, _} = Resp) ->
6464
Resp.
6565

66-
%% cowboy_req:reply/4 has a faulty spec in case of response body fun.
67-
%-dialyzer({[no_contracts, no_fail_call], send_oops_resp/4}).
68-
6966
send_oops_resp(Code, Headers, undefined, Req) ->
70-
{Code, Headers, Req};
67+
{response, Code, Headers, Req};
7168
send_oops_resp(Code, Headers0, File, _) ->
7269
FileSize = filelib:file_size(File),
7370
Headers = maps:merge(Headers0, #{

0 commit comments

Comments
 (0)