mirror of
https://github.com/valitydev/bouncer-client-erlang.git
synced 2024-11-06 00:25:18 +00:00
MSPF-621: Bouncer client (#1)
* added files * inited sub modules * added mock test * updated from url shortener * added tests * fixed format * Fix lint * fixed compose * added requested changes * added more itfs * removed service name, changed test to wc * returned service name Co-authored-by: Sergey Yelin <elinsn@gmail.com>
This commit is contained in:
parent
878de0942d
commit
d053efc9a3
22
Jenkinsfile
vendored
Normal file
22
Jenkinsfile
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
#!groovy
|
||||
// -*- mode: groovy -*-
|
||||
|
||||
def finalHook = {
|
||||
runStage('store CT logs') {
|
||||
archive '_build/test/logs/'
|
||||
}
|
||||
}
|
||||
|
||||
build('bouncer_client_erlang', 'docker-host', finalHook) {
|
||||
checkoutRepo()
|
||||
loadBuildUtils()
|
||||
|
||||
def pipeErlangLib
|
||||
runStage('load pipeline') {
|
||||
env.JENKINS_LIB = "build_utils/jenkins_lib"
|
||||
env.SH_TOOLS = "build_utils/sh"
|
||||
pipeErlangLib = load("${env.JENKINS_LIB}/pipeErlangLib.groovy")
|
||||
}
|
||||
|
||||
pipeErlangLib.runPipe(false)
|
||||
}
|
64
Makefile
Normal file
64
Makefile
Normal file
@ -0,0 +1,64 @@
|
||||
REBAR := $(shell which rebar3 2>/dev/null || which ./rebar3)
|
||||
SUBMODULES = build_utils
|
||||
SUBTARGETS = $(patsubst %,%/.git,$(SUBMODULES))
|
||||
|
||||
UTILS_PATH := build_utils
|
||||
TEMPLATES_PATH := .
|
||||
|
||||
# Name of the service
|
||||
SERVICE_NAME := bouncer_client_erlang
|
||||
|
||||
BUILD_IMAGE_TAG := 0c638a682f4735a65ef232b81ed872ba494574c3
|
||||
|
||||
CALL_ANYWHERE := \
|
||||
submodules \
|
||||
all compile xref lint dialyze test cover \
|
||||
clean distclean \
|
||||
check_format format
|
||||
|
||||
CALL_W_CONTAINER := $(CALL_ANYWHERE)
|
||||
|
||||
.PHONY: $(CALL_W_CONTAINER) all
|
||||
|
||||
all: compile
|
||||
|
||||
-include $(UTILS_PATH)/make_lib/utils_container.mk
|
||||
|
||||
$(SUBTARGETS): %/.git: %
|
||||
git submodule update --init $<
|
||||
touch $@
|
||||
|
||||
submodules: $(SUBTARGETS)
|
||||
|
||||
compile:
|
||||
$(REBAR) compile
|
||||
|
||||
xref:
|
||||
$(REBAR) xref
|
||||
|
||||
lint:
|
||||
elvis rock
|
||||
|
||||
check_format:
|
||||
$(REBAR) fmt -c
|
||||
|
||||
format:
|
||||
$(REBAR) fmt -w
|
||||
|
||||
dialyze:
|
||||
$(REBAR) dialyzer
|
||||
|
||||
clean:
|
||||
$(REBAR) cover -r
|
||||
$(REBAR) clean
|
||||
|
||||
distclean:
|
||||
$(REBAR) clean
|
||||
rm -rf _build
|
||||
|
||||
cover:
|
||||
$(REBAR) cover
|
||||
|
||||
# CALL_W_CONTAINER
|
||||
test:
|
||||
$(REBAR) ct
|
1
build_utils
Submodule
1
build_utils
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit f42e059d9ec93826ba4ad23232eed8ce67bd5486
|
55
elvis.config
Normal file
55
elvis.config
Normal file
@ -0,0 +1,55 @@
|
||||
[
|
||||
{elvis, [
|
||||
{config, [
|
||||
#{
|
||||
dirs => [
|
||||
"src",
|
||||
"test"
|
||||
],
|
||||
filter => "*.erl",
|
||||
ignore => ["_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 => []}},
|
||||
{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 => []
|
||||
}},
|
||||
{elvis_style, no_debug_call, #{}}
|
||||
]
|
||||
},
|
||||
#{
|
||||
dirs => ["."],
|
||||
filter => "Makefile",
|
||||
ruleset => makefiles
|
||||
},
|
||||
#{
|
||||
dirs => ["."],
|
||||
filter => "elvis.config",
|
||||
ruleset => elvis_config
|
||||
},
|
||||
#{
|
||||
dirs => ["."],
|
||||
filter => "rebar.config",
|
||||
rules => [
|
||||
{elvis_style, line_length, #{limit => 120, skip_comments => false}},
|
||||
{elvis_style, no_tabs},
|
||||
{elvis_style, no_trailing_whitespace}
|
||||
]
|
||||
}
|
||||
]}
|
||||
]}
|
||||
].
|
88
rebar.config
Normal file
88
rebar.config
Normal file
@ -0,0 +1,88 @@
|
||||
%% 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
|
||||
]}.
|
||||
|
||||
%% Common project dependencies.
|
||||
{deps, [
|
||||
{genlib,
|
||||
{git, "https://github.com/rbkmoney/genlib.git",
|
||||
{branch, "master"}
|
||||
}
|
||||
},
|
||||
{bouncer_proto,
|
||||
{git, "git@github.com:rbkmoney/bouncer-proto.git",
|
||||
{branch, "master"}
|
||||
}
|
||||
},
|
||||
{org_management_proto,
|
||||
{git, "git@github.com:rbkmoney/org-management-proto.git",
|
||||
{branch, "master"}
|
||||
}
|
||||
},
|
||||
{scoper,
|
||||
{git, "git@github.com:rbkmoney/scoper.git",
|
||||
{branch, master}
|
||||
}
|
||||
},
|
||||
{woody,
|
||||
{git, "git@github.com:rbkmoney/woody_erlang.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}.
|
||||
|
||||
{dialyzer, [
|
||||
{warnings, [
|
||||
% mandatory
|
||||
unmatched_returns,
|
||||
error_handling,
|
||||
race_conditions,
|
||||
unknown
|
||||
]},
|
||||
{plt_apps, all_deps}
|
||||
]}.
|
||||
|
||||
{plugins, [
|
||||
{erlfmt, "0.8.0"}
|
||||
]}.
|
||||
|
||||
{erlfmt, [
|
||||
{print_width, 120},
|
||||
{files, "{src,include,test}/*.{hrl,erl}"}
|
||||
]}.
|
72
rebar.lock
Normal file
72
rebar.lock
Normal file
@ -0,0 +1,72 @@
|
||||
{"1.1.0",
|
||||
[{<<"bear">>,{pkg,<<"bear">>,<<"0.8.7">>},3},
|
||||
{<<"bouncer_proto">>,
|
||||
{git,"git@github.com:rbkmoney/bouncer-proto.git",
|
||||
{ref,"298356b934e097393593785560c04bfa152ea0b5"}},
|
||||
0},
|
||||
{<<"cache">>,{pkg,<<"cache">>,<<"2.2.0">>},1},
|
||||
{<<"certifi">>,{pkg,<<"certifi">>,<<"2.5.1">>},2},
|
||||
{<<"cg_mon">>,
|
||||
{git,"https://github.com/rbkmoney/cg_mon.git",
|
||||
{ref,"5a87a37694e42b6592d3b4164ae54e0e87e24e18"}},
|
||||
2},
|
||||
{<<"cowboy">>,{pkg,<<"cowboy">>,<<"2.7.0">>},1},
|
||||
{<<"cowlib">>,{pkg,<<"cowlib">>,<<"2.8.0">>},2},
|
||||
{<<"folsom">>,
|
||||
{git,"https://github.com/folsom-project/folsom.git",
|
||||
{ref,"eeb1cc467eb64bd94075b95b8963e80d8b4df3df"}},
|
||||
2},
|
||||
{<<"genlib">>,
|
||||
{git,"https://github.com/rbkmoney/genlib.git",
|
||||
{ref,"7637d915c4c769f7f45c99f8688b17922e801027"}},
|
||||
0},
|
||||
{<<"gproc">>,{pkg,<<"gproc">>,<<"0.8.0">>},1},
|
||||
{<<"hackney">>,{pkg,<<"hackney">>,<<"1.15.2">>},1},
|
||||
{<<"how_are_you">>,
|
||||
{git,"https://github.com/rbkmoney/how_are_you.git",
|
||||
{ref,"8f11d17eeb6eb74096da7363a9df272fd3099718"}},
|
||||
1},
|
||||
{<<"idna">>,{pkg,<<"idna">>,<<"6.0.0">>},2},
|
||||
{<<"metrics">>,{pkg,<<"metrics">>,<<"1.0.1">>},2},
|
||||
{<<"mimerl">>,{pkg,<<"mimerl">>,<<"1.2.0">>},2},
|
||||
{<<"org_management_proto">>,
|
||||
{git,"git@github.com:rbkmoney/org-management-proto.git",
|
||||
{ref,"06c5c8430e445cb7874e54358e457cbb5697fc32"}},
|
||||
0},
|
||||
{<<"parse_trans">>,{pkg,<<"parse_trans">>,<<"3.3.0">>},3},
|
||||
{<<"ranch">>,{pkg,<<"ranch">>,<<"1.7.1">>},2},
|
||||
{<<"scoper">>,
|
||||
{git,"git@github.com:rbkmoney/scoper.git",
|
||||
{ref,"89e1af7422199ea3fa287207300bed1d6e00e5ab"}},
|
||||
0},
|
||||
{<<"snowflake">>,
|
||||
{git,"https://github.com/rbkmoney/snowflake.git",
|
||||
{ref,"7f379ad5e389e1c96389a8d60bae8117965d6a6d"}},
|
||||
1},
|
||||
{<<"ssl_verify_fun">>,{pkg,<<"ssl_verify_fun">>,<<"1.1.5">>},2},
|
||||
{<<"thrift">>,
|
||||
{git,"https://github.com/rbkmoney/thrift_erlang.git",
|
||||
{ref,"4eda678c985d2894251b91ae43aacf7941846cc9"}},
|
||||
1},
|
||||
{<<"unicode_util_compat">>,{pkg,<<"unicode_util_compat">>,<<"0.4.1">>},3},
|
||||
{<<"woody">>,
|
||||
{git,"git@github.com:rbkmoney/woody_erlang.git",
|
||||
{ref,"d106ef66bdd9ac303e05e1d5cddde85e0fa5f36a"}},
|
||||
0}]}.
|
||||
[
|
||||
{pkg_hash,[
|
||||
{<<"bear">>, <<"16264309AE5D005D03718A5C82641FCC259C9E8F09ADEB6FD79CA4271168656F">>},
|
||||
{<<"cache">>, <<"3C11DBF4CD8FCD5787C95A5FB2A04038E3729CFCA0386016EEA8C953AB48A5AB">>},
|
||||
{<<"certifi">>, <<"867CE347F7C7D78563450A18A6A28A8090331E77FA02380B4A21962A65D36EE5">>},
|
||||
{<<"cowboy">>, <<"91ED100138A764355F43316B1D23D7FF6BDB0DE4EA618CB5D8677C93A7A2F115">>},
|
||||
{<<"cowlib">>, <<"FD0FF1787DB84AC415B8211573E9A30A3EBE71B5CBFF7F720089972B2319C8A4">>},
|
||||
{<<"gproc">>, <<"CEA02C578589C61E5341FCE149EA36CCEF236CC2ECAC8691FBA408E7EA77EC2F">>},
|
||||
{<<"hackney">>, <<"07E33C794F8F8964EE86CEBEC1A8ED88DB5070E52E904B8F12209773C1036085">>},
|
||||
{<<"idna">>, <<"689C46CBCDF3524C44D5F3DDE8001F364CD7608A99556D8FBD8239A5798D4C10">>},
|
||||
{<<"metrics">>, <<"25F094DEA2CDA98213CECC3AEFF09E940299D950904393B2A29D191C346A8486">>},
|
||||
{<<"mimerl">>, <<"67E2D3F571088D5CFD3E550C383094B47159F3EEE8FFA08E64106CDF5E981BE3">>},
|
||||
{<<"parse_trans">>, <<"09765507A3C7590A784615CFD421D101AEC25098D50B89D7AA1D66646BC571C1">>},
|
||||
{<<"ranch">>, <<"6B1FAB51B49196860B733A49C07604465A47BDB78AA10C1C16A3D199F7F8C881">>},
|
||||
{<<"ssl_verify_fun">>, <<"6EAF7AD16CB568BB01753DBBD7A95FF8B91C7979482B95F38443FE2C8852A79B">>},
|
||||
{<<"unicode_util_compat">>, <<"D869E4C68901DD9531385BB0C8C40444EBF624E60B6962D95952775CAC5E90CD">>}]}
|
||||
].
|
21
src/bouncer_client.app.src
Normal file
21
src/bouncer_client.app.src
Normal file
@ -0,0 +1,21 @@
|
||||
{application, bouncer_client, [
|
||||
{description, "Bouncer service client"},
|
||||
{vsn, "0.1.0"},
|
||||
{registered, []},
|
||||
{applications, [
|
||||
kernel,
|
||||
stdlib,
|
||||
genlib,
|
||||
bouncer_proto,
|
||||
org_management_proto,
|
||||
scoper,
|
||||
woody
|
||||
]},
|
||||
{env, []},
|
||||
{modules, []},
|
||||
{maintainers, [
|
||||
""
|
||||
]},
|
||||
{licenses, []},
|
||||
{links, []}
|
||||
]}.
|
100
src/bouncer_client.erl
Normal file
100
src/bouncer_client.erl
Normal file
@ -0,0 +1,100 @@
|
||||
-module(bouncer_client).
|
||||
|
||||
-include_lib("bouncer_proto/include/bouncer_decisions_thrift.hrl").
|
||||
-include_lib("bouncer_proto/include/bouncer_context_v1_thrift.hrl").
|
||||
-include_lib("bouncer_proto/include/bouncer_context_thrift.hrl").
|
||||
|
||||
% -include_lib("org_management_proto/include/orgmgmt_auth_context_provider_thrift.hrl").
|
||||
|
||||
%% API
|
||||
|
||||
-export([judge/3]).
|
||||
|
||||
%%
|
||||
|
||||
-type woody_context() :: woody_context:ctx().
|
||||
|
||||
-type context_fragment_id() :: binary().
|
||||
-type ruleset_id() :: binary().
|
||||
-type bouncer_fragment() :: bouncer_context_v1_thrift:'ContextFragment'().
|
||||
-type encoded_bouncer_fragment() :: bouncer_context_thrift:'ContextFragment'().
|
||||
-type context_fragment() ::
|
||||
{fragment, bouncer_fragment()}
|
||||
| {encoded_fragment, encoded_bouncer_fragment()}.
|
||||
|
||||
-type judge_context() :: #{
|
||||
fragments => #{context_fragment_id() => context_fragment()}
|
||||
}.
|
||||
|
||||
-type judgement() :: allowed | forbidden.
|
||||
|
||||
-type service_name() :: atom().
|
||||
|
||||
-export_type([service_name/0]).
|
||||
-export_type([judgement/0]).
|
||||
-export_type([judge_context/0]).
|
||||
-export_type([context_fragment/0]).
|
||||
|
||||
-spec judge(ruleset_id(), judge_context(), woody_context()) -> judgement().
|
||||
judge(RulesetID, JudgeContext, WoodyContext) ->
|
||||
case judge_(RulesetID, JudgeContext, WoodyContext) of
|
||||
{ok, Judgement} ->
|
||||
Judgement;
|
||||
{error, Reason} ->
|
||||
erlang:error({bouncer_judgement_failed, Reason})
|
||||
end.
|
||||
|
||||
-spec judge_(ruleset_id(), judge_context(), woody_context()) ->
|
||||
{ok, judgement()}
|
||||
| {error,
|
||||
{ruleset, notfound | invalid}
|
||||
| {context, invalid}}.
|
||||
judge_(RulesetID, JudgeContext, WoodyContext) ->
|
||||
Context = collect_judge_context(JudgeContext),
|
||||
case bouncer_client_woody:call(bouncer, 'Judge', {RulesetID, Context}, WoodyContext) of
|
||||
{ok, Judgement} ->
|
||||
{ok, parse_judgement(Judgement)};
|
||||
{exception, #bdcs_RulesetNotFound{}} ->
|
||||
{error, {ruleset, notfound}};
|
||||
{exception, #bdcs_InvalidRuleset{}} ->
|
||||
{error, {ruleset, invalid}};
|
||||
{exception, #bdcs_InvalidContext{}} ->
|
||||
{error, {context, invalid}}
|
||||
end.
|
||||
|
||||
%%
|
||||
|
||||
collect_judge_context(JudgeContext) ->
|
||||
#bdcs_Context{fragments = collect_fragments(JudgeContext, #{})}.
|
||||
|
||||
collect_fragments(#{fragments := Fragments}, Context) ->
|
||||
maps:fold(fun collect_fragments_/3, Context, Fragments);
|
||||
collect_fragments(_, Context) ->
|
||||
Context.
|
||||
|
||||
collect_fragments_(FragmentID, {encoded_fragment, EncodedFragment}, Acc0) ->
|
||||
Acc0#{FragmentID => EncodedFragment};
|
||||
collect_fragments_(FragmentID, {fragment, Fragment}, Acc0) ->
|
||||
Acc0#{
|
||||
FragmentID => #bctx_ContextFragment{
|
||||
type = v1_thrift_binary,
|
||||
content = encode_context_fragment(Fragment)
|
||||
}
|
||||
}.
|
||||
|
||||
%%
|
||||
|
||||
parse_judgement(#bdcs_Judgement{resolution = allowed}) ->
|
||||
allowed;
|
||||
parse_judgement(#bdcs_Judgement{resolution = forbidden}) ->
|
||||
forbidden.
|
||||
|
||||
%%
|
||||
|
||||
encode_context_fragment(ContextFragment) ->
|
||||
Type = {struct, struct, {bouncer_context_v1_thrift, 'ContextFragment'}},
|
||||
Codec = thrift_strict_binary_codec:new(),
|
||||
case thrift_strict_binary_codec:write(Codec, Type, ContextFragment) of
|
||||
{ok, Codec1} ->
|
||||
thrift_strict_binary_codec:close(Codec1)
|
||||
end.
|
94
src/bouncer_client_woody.erl
Normal file
94
src/bouncer_client_woody.erl
Normal file
@ -0,0 +1,94 @@
|
||||
-module(bouncer_client_woody).
|
||||
|
||||
-export([call/4]).
|
||||
-export([call/5]).
|
||||
|
||||
-define(APP, bouncer_client).
|
||||
-define(DEFAULT_DEADLINE, 5000).
|
||||
|
||||
%%
|
||||
-type service_name() :: atom().
|
||||
|
||||
-spec call(service_name(), woody:func(), woody:args(), woody_context:ctx()) -> woody:result().
|
||||
call(ServiceName, Function, Args, Context) ->
|
||||
EventHandler = scoper_woody_event_handler,
|
||||
call(ServiceName, Function, Args, Context, EventHandler).
|
||||
|
||||
-spec call(service_name(), woody:func(), woody:args(), woody_context:ctx(), woody:ev_handler()) -> woody:result().
|
||||
call(ServiceName, Function, Args, Context0, EventHandler) ->
|
||||
Deadline = get_service_deadline(ServiceName),
|
||||
Context1 = set_deadline(Deadline, Context0),
|
||||
Retry = get_service_retry(ServiceName, Function),
|
||||
call(ServiceName, Function, Args, Context1, EventHandler, Retry).
|
||||
|
||||
call(ServiceName, Function, Args, Context, EventHandler, Retry) ->
|
||||
Url = get_service_client_url(ServiceName),
|
||||
Service = get_service_modname(ServiceName),
|
||||
Request = {Service, Function, Args},
|
||||
try
|
||||
woody_client:call(
|
||||
Request,
|
||||
#{url => Url, event_handler => EventHandler},
|
||||
Context
|
||||
)
|
||||
catch
|
||||
error:{woody_error, {_Source, Class, _Details}} = Error when
|
||||
Class =:= resource_unavailable orelse Class =:= result_unknown
|
||||
->
|
||||
NextRetry = apply_retry_strategy(Retry, Error, Context),
|
||||
call(ServiceName, Function, Args, Context, EventHandler, NextRetry)
|
||||
end.
|
||||
|
||||
apply_retry_strategy(Retry, Error, Context) ->
|
||||
apply_retry_step(genlib_retry:next_step(Retry), woody_context:get_deadline(Context), Error).
|
||||
|
||||
apply_retry_step(finish, _, Error) ->
|
||||
erlang:error(Error);
|
||||
apply_retry_step({wait, Timeout, Retry}, undefined, _) ->
|
||||
ok = timer:sleep(Timeout),
|
||||
Retry;
|
||||
apply_retry_step({wait, Timeout, Retry}, Deadline0, Error) ->
|
||||
Deadline1 = woody_deadline:from_unixtime_ms(
|
||||
woody_deadline:to_unixtime_ms(Deadline0) - Timeout
|
||||
),
|
||||
case woody_deadline:is_reached(Deadline1) of
|
||||
true ->
|
||||
% no more time for retries
|
||||
erlang:error(Error);
|
||||
false ->
|
||||
ok = timer:sleep(Timeout),
|
||||
Retry
|
||||
end.
|
||||
|
||||
get_service_client_config(ServiceName) ->
|
||||
ServiceClients = genlib_app:env(bouncer_client, service_clients, #{}),
|
||||
maps:get(ServiceName, ServiceClients, #{}).
|
||||
|
||||
get_service_client_url(ServiceName) ->
|
||||
maps:get(url, get_service_client_config(ServiceName), undefined).
|
||||
|
||||
-spec get_service_modname(service_name()) -> woody:service().
|
||||
get_service_modname(org_management) ->
|
||||
{orgmgmt_auth_context_provider_thrift, 'AuthContextProvider'};
|
||||
get_service_modname(bouncer) ->
|
||||
{bouncer_decisions_thrift, 'Arbiter'}.
|
||||
|
||||
-spec get_service_deadline(service_name()) -> undefined | woody_deadline:deadline().
|
||||
get_service_deadline(ServiceName) ->
|
||||
ServiceClient = get_service_client_config(ServiceName),
|
||||
Timeout = maps:get(deadline, ServiceClient, ?DEFAULT_DEADLINE),
|
||||
woody_deadline:from_timeout(Timeout).
|
||||
|
||||
set_deadline(Deadline, Context) ->
|
||||
case woody_context:get_deadline(Context) of
|
||||
undefined ->
|
||||
woody_context:set_deadline(Deadline, Context);
|
||||
_AlreadySet ->
|
||||
Context
|
||||
end.
|
||||
|
||||
get_service_retry(ServiceName, Function) ->
|
||||
ServiceRetries = genlib_app:env(?APP, service_retries, #{}),
|
||||
FunctionReties = maps:get(ServiceName, ServiceRetries, #{}),
|
||||
DefaultRetry = maps:get('_', FunctionReties, finish),
|
||||
maps:get(Function, FunctionReties, DefaultRetry).
|
235
src/bouncer_context_helpers.erl
Normal file
235
src/bouncer_context_helpers.erl
Normal file
@ -0,0 +1,235 @@
|
||||
-module(bouncer_context_helpers).
|
||||
|
||||
-include_lib("bouncer_proto/include/bouncer_decisions_thrift.hrl").
|
||||
-include_lib("bouncer_proto/include/bouncer_context_v1_thrift.hrl").
|
||||
-include_lib("bouncer_proto/include/bouncer_context_thrift.hrl").
|
||||
|
||||
-export([make_default_env_context_fragment/0]).
|
||||
-export([make_env_context_fragment/1]).
|
||||
-export([make_auth_context_fragment/1]).
|
||||
-export([make_default_user_context_fragment/1]).
|
||||
-export([make_user_context_fragment/1]).
|
||||
-export([make_requester_context_fragment/1]).
|
||||
-export([get_user_context_fragment/2]).
|
||||
|
||||
-type id() :: binary().
|
||||
-type method() :: binary().
|
||||
-type email() :: binary().
|
||||
-type timestamp() :: binary().
|
||||
-type ip() :: string().
|
||||
-type context_fragment() :: bouncer_client:context_fragment().
|
||||
-type woody_context() :: woody_context:ctx().
|
||||
|
||||
-type entity() :: #{
|
||||
id => id()
|
||||
}.
|
||||
|
||||
-type environment_params() :: #{
|
||||
now => timestamp(),
|
||||
deployment => deployment()
|
||||
}.
|
||||
|
||||
-type deployment() :: #{
|
||||
id => id()
|
||||
}.
|
||||
|
||||
-type auth_params() :: #{
|
||||
method => method(),
|
||||
scope => [auth_scope()],
|
||||
expiration => timestamp()
|
||||
}.
|
||||
|
||||
-type auth_scope() :: #{
|
||||
party => entity(),
|
||||
shop => entity(),
|
||||
invoice => entity()
|
||||
}.
|
||||
|
||||
-type user_params() :: #{
|
||||
id => id(),
|
||||
realm => entity(),
|
||||
email => email(),
|
||||
orgs => [user_org()]
|
||||
}.
|
||||
|
||||
-type user_org() :: #{
|
||||
id => id(),
|
||||
owner => entity(),
|
||||
roles => [user_role()]
|
||||
}.
|
||||
|
||||
-type user_role() :: #{
|
||||
id => id(),
|
||||
scope => user_scope()
|
||||
}.
|
||||
|
||||
-type user_scope() :: #{
|
||||
shop => entity()
|
||||
}.
|
||||
|
||||
-type requester_params() :: #{
|
||||
ip => ip()
|
||||
}.
|
||||
|
||||
-export_type([environment_params/0]).
|
||||
-export_type([auth_params/0]).
|
||||
-export_type([user_params/0]).
|
||||
-export_type([requester_params/0]).
|
||||
|
||||
-spec make_default_env_context_fragment() -> context_fragment().
|
||||
make_default_env_context_fragment() ->
|
||||
Params = #{
|
||||
now => genlib_rfc3339:format(genlib_time:unow(), second)
|
||||
},
|
||||
make_env_context_fragment(Params).
|
||||
|
||||
-spec make_env_context_fragment(environment_params()) -> context_fragment().
|
||||
make_env_context_fragment(Params) ->
|
||||
Datetime = maybe_get_param(now, Params),
|
||||
Deployment = maybe_get_param(deployment, Params),
|
||||
DeploymentID = maybe_get_param(id, Deployment),
|
||||
|
||||
{fragment, #bctx_v1_ContextFragment{
|
||||
env = #bctx_v1_Environment{
|
||||
now = Datetime,
|
||||
deployment = maybe_add_param(#bctx_v1_Deployment{id = DeploymentID}, Deployment)
|
||||
}
|
||||
}}.
|
||||
|
||||
-spec make_auth_context_fragment(auth_params()) -> context_fragment().
|
||||
make_auth_context_fragment(Params) ->
|
||||
Method = maybe_get_param(method, Params),
|
||||
Scope = maybe_get_param(scope, Params),
|
||||
Expiration = maybe_get_param(expiration, Params),
|
||||
|
||||
{fragment, #bctx_v1_ContextFragment{
|
||||
auth = #bctx_v1_Auth{
|
||||
method = Method,
|
||||
scope = maybe_marshal_auth_scopes(Scope),
|
||||
expiration = Expiration
|
||||
}
|
||||
}}.
|
||||
|
||||
-spec make_default_user_context_fragment(id()) -> context_fragment().
|
||||
make_default_user_context_fragment(UserID) ->
|
||||
{fragment, #bctx_v1_ContextFragment{
|
||||
user = #bctx_v1_User{
|
||||
id = UserID
|
||||
}
|
||||
}}.
|
||||
|
||||
-spec make_user_context_fragment(user_params()) -> context_fragment().
|
||||
make_user_context_fragment(Params) ->
|
||||
UserID = maybe_get_param(id, Params),
|
||||
RealmEntity = maybe_get_param(realm, Params),
|
||||
Email = maybe_get_param(email, Params),
|
||||
Orgs = maybe_get_param(orgs, Params),
|
||||
|
||||
{fragment, #bctx_v1_ContextFragment{
|
||||
user = #bctx_v1_User{
|
||||
id = UserID,
|
||||
realm = maybe_add_param(maybe_marshal_entity(RealmEntity), RealmEntity),
|
||||
email = Email,
|
||||
orgs = maybe_add_param(maybe_marshal_user_orgs(Orgs), Orgs)
|
||||
}
|
||||
}}.
|
||||
|
||||
-spec make_requester_context_fragment(requester_params()) -> context_fragment().
|
||||
make_requester_context_fragment(Params) ->
|
||||
IP = maybe_get_param(ip, Params),
|
||||
|
||||
{fragment, #bctx_v1_ContextFragment{
|
||||
requester = #bctx_v1_Requester{
|
||||
ip = maybe_marshal_ip(IP)
|
||||
}
|
||||
}}.
|
||||
|
||||
-spec get_user_context_fragment(id(), woody_context()) -> {ok, context_fragment()} | {error, {user, notfound}}.
|
||||
get_user_context_fragment(UserID, WoodyContext) ->
|
||||
ServiceName = org_management,
|
||||
case bouncer_client_woody:call(ServiceName, 'GetUserContext', {UserID}, WoodyContext) of
|
||||
{ok, EncodedFragment} ->
|
||||
{ok, {encoded_fragment, convert_fragment(ServiceName, EncodedFragment)}};
|
||||
{exception, {orgmgmt_UserNotFound}} ->
|
||||
{error, {user, notfound}}
|
||||
end.
|
||||
|
||||
%%
|
||||
|
||||
convert_fragment(org_management, {bctx_ContextFragment, Type = v1_thrift_binary, Content}) when is_binary(Content) ->
|
||||
#bctx_ContextFragment{
|
||||
type = Type,
|
||||
content = Content
|
||||
}.
|
||||
|
||||
maybe_get_param(_Key, undefined) ->
|
||||
undefined;
|
||||
maybe_get_param(Key, Map) ->
|
||||
maps:get(Key, Map, undefined).
|
||||
|
||||
maybe_add_param(_Value, undefined) ->
|
||||
undefined;
|
||||
maybe_add_param(Value, _Param) ->
|
||||
Value.
|
||||
|
||||
maybe_marshal_entity(undefined) ->
|
||||
undefined;
|
||||
maybe_marshal_entity(Entity) ->
|
||||
EntityID = maybe_get_param(id, Entity),
|
||||
#bctx_v1_Entity{id = EntityID}.
|
||||
|
||||
maybe_marshal_auth_scopes(undefined) ->
|
||||
undefined;
|
||||
maybe_marshal_auth_scopes(Scopes) ->
|
||||
lists:map(fun(Scope) -> maybe_marshal_auth_scope(Scope) end, Scopes).
|
||||
|
||||
maybe_marshal_auth_scope(Scope) ->
|
||||
PartyEntity = maybe_get_param(party, Scope),
|
||||
ShopEntity = maybe_get_param(shop, Scope),
|
||||
InvoiceEntity = maybe_get_param(invoice, Scope),
|
||||
#bctx_v1_AuthScope{
|
||||
party = maybe_add_param(maybe_marshal_entity(PartyEntity), PartyEntity),
|
||||
shop = maybe_add_param(maybe_marshal_entity(ShopEntity), ShopEntity),
|
||||
invoice = maybe_add_param(maybe_marshal_entity(InvoiceEntity), InvoiceEntity)
|
||||
}.
|
||||
|
||||
maybe_marshal_user_orgs(undefined) ->
|
||||
undefined;
|
||||
maybe_marshal_user_orgs(Orgs) ->
|
||||
lists:map(fun(Org) -> maybe_marshal_user_org(Org) end, Orgs).
|
||||
|
||||
maybe_marshal_user_org(Org) ->
|
||||
ID = maybe_get_param(id, Org),
|
||||
OwnerEntity = maybe_get_param(owner, Org),
|
||||
Roles = maybe_get_param(roles, Org),
|
||||
|
||||
#bctx_v1_Organization{
|
||||
id = ID,
|
||||
owner = maybe_add_param(maybe_marshal_entity(OwnerEntity), OwnerEntity),
|
||||
roles = maybe_add_param(maybe_marshal_user_roles(Roles), Roles)
|
||||
}.
|
||||
|
||||
maybe_marshal_user_roles(undefined) ->
|
||||
undefined;
|
||||
maybe_marshal_user_roles(Roles) ->
|
||||
lists:map(fun(Role) -> maybe_marshal_user_role(Role) end, Roles).
|
||||
|
||||
maybe_marshal_user_role(Role) ->
|
||||
ID = maybe_get_param(id, Role),
|
||||
Scope = maybe_get_param(scope, Role),
|
||||
ShopEntity = maybe_get_param(shop, Scope),
|
||||
|
||||
#bctx_v1_OrgRole{
|
||||
id = ID,
|
||||
scope = maybe_add_param(
|
||||
#bctx_v1_OrgRoleScope{
|
||||
shop = maybe_add_param(maybe_marshal_entity(ShopEntity), ShopEntity)
|
||||
},
|
||||
Scope
|
||||
)
|
||||
}.
|
||||
|
||||
maybe_marshal_ip(undefined) ->
|
||||
undefined;
|
||||
maybe_marshal_ip(IP) ->
|
||||
list_to_binary(IP).
|
423
test/bouncer_client_SUITE.erl
Normal file
423
test/bouncer_client_SUITE.erl
Normal file
@ -0,0 +1,423 @@
|
||||
-module(bouncer_client_SUITE).
|
||||
|
||||
-include_lib("stdlib/include/assert.hrl").
|
||||
-include_lib("common_test/include/ct.hrl").
|
||||
|
||||
-include_lib("bouncer_proto/include/bouncer_decisions_thrift.hrl").
|
||||
-include_lib("bouncer_proto/include/bouncer_context_v1_thrift.hrl").
|
||||
|
||||
-export([all/0]).
|
||||
|
||||
-export([groups/0]).
|
||||
-export([init_per_suite/1]).
|
||||
-export([end_per_suite/1]).
|
||||
-export([init_per_testcase/2]).
|
||||
-export([end_per_testcase/2]).
|
||||
|
||||
-export([empty_judge/1]).
|
||||
-export([validate_default_user_fragment/1]).
|
||||
-export([validate_user_fragment/1]).
|
||||
-export([validate_env_fragment/1]).
|
||||
-export([validate_auth_fragment/1]).
|
||||
-export([validate_requester_fragment/1]).
|
||||
-export([validate_remote_user_fragment/1]).
|
||||
|
||||
-type test_case_name() :: atom().
|
||||
|
||||
-define(RULESET_ID, <<"service/authz/api">>).
|
||||
|
||||
%% tests descriptions
|
||||
|
||||
-spec all() -> [test_case_name()].
|
||||
all() ->
|
||||
[
|
||||
{group, default}
|
||||
].
|
||||
|
||||
-spec groups() -> [{atom(), list(), [test_case_name()]}].
|
||||
groups() ->
|
||||
[
|
||||
{default, [], [
|
||||
empty_judge,
|
||||
validate_default_user_fragment,
|
||||
validate_user_fragment,
|
||||
validate_env_fragment,
|
||||
validate_auth_fragment,
|
||||
validate_requester_fragment,
|
||||
validate_remote_user_fragment
|
||||
]}
|
||||
].
|
||||
|
||||
-type config() :: [{atom(), any()}].
|
||||
|
||||
-spec init_per_suite(config()) -> config().
|
||||
init_per_suite(Config) ->
|
||||
Apps =
|
||||
genlib_app:start_application_with(bouncer_client, [
|
||||
{service_clients, #{
|
||||
bouncer => #{
|
||||
url => <<"http://bouncer:8022/">>,
|
||||
retries => #{
|
||||
'Judge' => {linear, 3, 1000},
|
||||
'_' => finish
|
||||
}
|
||||
},
|
||||
org_management => #{
|
||||
url => <<"http://org_management:8022/">>,
|
||||
retries => #{
|
||||
% function => retry strategy
|
||||
% '_' work as "any"
|
||||
% default value is 'finish'
|
||||
% for more info look genlib_retry :: strategy()
|
||||
% https://github.com/rbkmoney/genlib/blob/master/src/genlib_retry.erl#L19
|
||||
'GetUserContext' => {linear, 3, 1000},
|
||||
'_' => finish
|
||||
}
|
||||
}
|
||||
}}
|
||||
]),
|
||||
[{apps, Apps}] ++ Config.
|
||||
|
||||
-spec end_per_suite(config()) -> _.
|
||||
end_per_suite(Config) ->
|
||||
[application:stop(App) || App <- proplists:get_value(apps, Config)],
|
||||
Config.
|
||||
|
||||
-spec init_per_testcase(test_case_name(), config()) -> config().
|
||||
init_per_testcase(_Name, C) ->
|
||||
[{test_sup, start_mocked_service_sup()} | C].
|
||||
|
||||
-spec end_per_testcase(test_case_name(), config()) -> config().
|
||||
end_per_testcase(_Name, C) ->
|
||||
stop_mocked_service_sup(?config(test_sup, C)),
|
||||
ok.
|
||||
|
||||
%%
|
||||
|
||||
-spec empty_judge(config()) -> _.
|
||||
empty_judge(C) ->
|
||||
mock_services(
|
||||
[
|
||||
{bouncer, fun('Judge', _) -> {ok, #bdcs_Judgement{resolution = allowed}} end}
|
||||
],
|
||||
C
|
||||
),
|
||||
WoodyContext = woody_context:new(),
|
||||
allowed = bouncer_client:judge(?RULESET_ID, #{}, WoodyContext).
|
||||
|
||||
-spec validate_default_user_fragment(config()) -> _.
|
||||
validate_default_user_fragment(C) ->
|
||||
UserID = <<"someUser">>,
|
||||
mock_services(
|
||||
[
|
||||
{bouncer, fun('Judge', {_RulesetID, Fragments}) ->
|
||||
case get_user_id(Fragments) of
|
||||
UserID ->
|
||||
{ok, #bdcs_Judgement{resolution = allowed}};
|
||||
_ ->
|
||||
{ok, #bdcs_Judgement{resolution = forbidden}}
|
||||
end
|
||||
end}
|
||||
],
|
||||
C
|
||||
),
|
||||
WoodyContext = woody_context:new(),
|
||||
allowed = bouncer_client:judge(
|
||||
?RULESET_ID,
|
||||
#{fragments => #{<<"user">> => bouncer_context_helpers:make_default_user_context_fragment(UserID)}},
|
||||
WoodyContext
|
||||
).
|
||||
|
||||
-spec validate_user_fragment(config()) -> _.
|
||||
validate_user_fragment(C) ->
|
||||
UserID = <<"someUser">>,
|
||||
mock_services(
|
||||
[
|
||||
{bouncer, fun('Judge', {_RulesetID, Fragments}) ->
|
||||
case get_user_id(Fragments) of
|
||||
UserID ->
|
||||
{ok, #bdcs_Judgement{resolution = allowed}};
|
||||
_ ->
|
||||
{ok, #bdcs_Judgement{resolution = forbidden}}
|
||||
end
|
||||
end}
|
||||
],
|
||||
C
|
||||
),
|
||||
WoodyContext = woody_context:new(),
|
||||
allowed = bouncer_client:judge(
|
||||
?RULESET_ID,
|
||||
#{fragments => #{<<"user">> => bouncer_context_helpers:make_user_context_fragment(#{id => UserID})}},
|
||||
WoodyContext
|
||||
).
|
||||
|
||||
-spec validate_env_fragment(config()) -> _.
|
||||
validate_env_fragment(C) ->
|
||||
Time = genlib_rfc3339:format(genlib_time:unow(), second),
|
||||
mock_services(
|
||||
[
|
||||
{bouncer, fun('Judge', {_RulesetID, Fragments}) ->
|
||||
case get_time(Fragments) of
|
||||
Time ->
|
||||
{ok, #bdcs_Judgement{resolution = allowed}};
|
||||
_ ->
|
||||
{ok, #bdcs_Judgement{resolution = forbidden}}
|
||||
end
|
||||
end}
|
||||
],
|
||||
C
|
||||
),
|
||||
WoodyContext = woody_context:new(),
|
||||
allowed = bouncer_client:judge(
|
||||
?RULESET_ID,
|
||||
#{fragments => #{<<"env">> => bouncer_context_helpers:make_env_context_fragment(#{now => Time})}},
|
||||
WoodyContext
|
||||
).
|
||||
|
||||
-spec validate_auth_fragment(config()) -> _.
|
||||
validate_auth_fragment(C) ->
|
||||
Method = <<"someMethod">>,
|
||||
mock_services(
|
||||
[
|
||||
{bouncer, fun('Judge', {_RulesetID, Fragments}) ->
|
||||
case get_auth_method(Fragments) of
|
||||
Method ->
|
||||
{ok, #bdcs_Judgement{resolution = allowed}};
|
||||
_ ->
|
||||
{ok, #bdcs_Judgement{resolution = forbidden}}
|
||||
end
|
||||
end}
|
||||
],
|
||||
C
|
||||
),
|
||||
WoodyContext = woody_context:new(),
|
||||
allowed = bouncer_client:judge(
|
||||
?RULESET_ID,
|
||||
#{fragments => #{<<"auth">> => bouncer_context_helpers:make_auth_context_fragment(#{method => Method})}},
|
||||
WoodyContext
|
||||
).
|
||||
|
||||
-spec validate_requester_fragment(config()) -> _.
|
||||
validate_requester_fragment(C) ->
|
||||
IP = "someIP",
|
||||
mock_services(
|
||||
[
|
||||
{bouncer, fun('Judge', {_RulesetID, Fragments}) ->
|
||||
case get_ip(Fragments) of
|
||||
undefined ->
|
||||
{ok, #bdcs_Judgement{resolution = forbidden}};
|
||||
BinaryIP ->
|
||||
case binary_to_list(BinaryIP) of
|
||||
IP ->
|
||||
{ok, #bdcs_Judgement{resolution = allowed}};
|
||||
_ ->
|
||||
{ok, #bdcs_Judgement{resolution = forbidden}}
|
||||
end
|
||||
end
|
||||
end}
|
||||
],
|
||||
C
|
||||
),
|
||||
WoodyContext = woody_context:new(),
|
||||
allowed = bouncer_client:judge(
|
||||
?RULESET_ID,
|
||||
#{fragments => #{<<"requester">> => bouncer_context_helpers:make_requester_context_fragment(#{ip => IP})}},
|
||||
WoodyContext
|
||||
).
|
||||
|
||||
-spec validate_remote_user_fragment(config()) -> _.
|
||||
validate_remote_user_fragment(C) ->
|
||||
UserID = <<"someUser">>,
|
||||
mock_services(
|
||||
[
|
||||
{org_management, fun('GetUserContext', _) ->
|
||||
Content = encode(#bctx_v1_ContextFragment{
|
||||
user = #bctx_v1_User{
|
||||
id = UserID
|
||||
}
|
||||
}),
|
||||
{ok, {bctx_ContextFragment, v1_thrift_binary, Content}}
|
||||
end},
|
||||
{bouncer, fun('Judge', {_RulesetID, Fragments}) ->
|
||||
case get_user_id(Fragments) of
|
||||
UserID ->
|
||||
{ok, #bdcs_Judgement{resolution = allowed}};
|
||||
_ ->
|
||||
{ok, #bdcs_Judgement{resolution = forbidden}}
|
||||
end
|
||||
end}
|
||||
],
|
||||
C
|
||||
),
|
||||
WoodyContext = woody_context:new(),
|
||||
{ok, EncodedUserFragment} = bouncer_context_helpers:get_user_context_fragment(UserID, WoodyContext),
|
||||
allowed = bouncer_client:judge(?RULESET_ID, #{fragments => #{<<"user">> => EncodedUserFragment}}, WoodyContext).
|
||||
|
||||
%%
|
||||
|
||||
get_ip(#bdcs_Context{
|
||||
fragments = #{
|
||||
<<"requester">> := #bctx_ContextFragment{
|
||||
type = v1_thrift_binary,
|
||||
content = Fragment
|
||||
}
|
||||
}
|
||||
}) ->
|
||||
case decode(Fragment) of
|
||||
{error, _} = Error ->
|
||||
error(Error);
|
||||
#bctx_v1_ContextFragment{requester = #bctx_v1_Requester{ip = IP}} ->
|
||||
IP
|
||||
end.
|
||||
|
||||
get_auth_method(#bdcs_Context{
|
||||
fragments = #{
|
||||
<<"auth">> := #bctx_ContextFragment{
|
||||
type = v1_thrift_binary,
|
||||
content = Fragment
|
||||
}
|
||||
}
|
||||
}) ->
|
||||
case decode(Fragment) of
|
||||
{error, _} = Error ->
|
||||
error(Error);
|
||||
#bctx_v1_ContextFragment{auth = #bctx_v1_Auth{method = Method}} ->
|
||||
Method
|
||||
end.
|
||||
|
||||
get_time(#bdcs_Context{
|
||||
fragments = #{
|
||||
<<"env">> := #bctx_ContextFragment{
|
||||
type = v1_thrift_binary,
|
||||
content = Fragment
|
||||
}
|
||||
}
|
||||
}) ->
|
||||
case decode(Fragment) of
|
||||
{error, _} = Error ->
|
||||
error(Error);
|
||||
#bctx_v1_ContextFragment{env = #bctx_v1_Environment{now = Time}} ->
|
||||
Time
|
||||
end.
|
||||
|
||||
get_user_id(#bdcs_Context{
|
||||
fragments = #{
|
||||
<<"user">> := #bctx_ContextFragment{
|
||||
type = v1_thrift_binary,
|
||||
content = Fragment
|
||||
}
|
||||
}
|
||||
}) ->
|
||||
case decode(Fragment) of
|
||||
{error, _} = Error ->
|
||||
error(Error);
|
||||
#bctx_v1_ContextFragment{user = #bctx_v1_User{id = UserID}} ->
|
||||
UserID
|
||||
end.
|
||||
|
||||
decode(Content) ->
|
||||
Type = {struct, struct, {bouncer_context_v1_thrift, 'ContextFragment'}},
|
||||
Codec = thrift_strict_binary_codec:new(Content),
|
||||
case thrift_strict_binary_codec:read(Codec, Type) of
|
||||
{ok, CtxThrift, Codec1} ->
|
||||
case thrift_strict_binary_codec:close(Codec1) of
|
||||
<<>> ->
|
||||
CtxThrift;
|
||||
Leftovers ->
|
||||
{error, {excess_binary_data, Leftovers}}
|
||||
end;
|
||||
Error ->
|
||||
Error
|
||||
end.
|
||||
|
||||
encode(ContextFragment) ->
|
||||
Type = {struct, struct, {bouncer_context_v1_thrift, 'ContextFragment'}},
|
||||
Codec = thrift_strict_binary_codec:new(),
|
||||
case thrift_strict_binary_codec:write(Codec, Type, ContextFragment) of
|
||||
{ok, Codec1} ->
|
||||
thrift_strict_binary_codec:close(Codec1)
|
||||
end.
|
||||
|
||||
%%
|
||||
|
||||
start_mocked_service_sup() ->
|
||||
{ok, SupPid} = genlib_adhoc_supervisor:start_link(#{}, []),
|
||||
_ = unlink(SupPid),
|
||||
SupPid.
|
||||
|
||||
-spec stop_mocked_service_sup(pid()) -> _.
|
||||
stop_mocked_service_sup(SupPid) ->
|
||||
exit(SupPid, shutdown).
|
||||
|
||||
-define(APP, bouncer_client).
|
||||
-define(HOST_IP, "::").
|
||||
-define(HOST_PORT, 8080).
|
||||
-define(HOST_NAME, "localhost").
|
||||
-define(HOST_URL, ?HOST_NAME ++ ":" ++ integer_to_list(?HOST_PORT)).
|
||||
|
||||
mock_services(Services, SupOrConfig) ->
|
||||
maps:map(fun set_cfg/2, mock_services_(Services, SupOrConfig)).
|
||||
|
||||
set_cfg(Service, Url) ->
|
||||
{ok, Clients} = application:get_env(?APP, service_clients),
|
||||
#{Service := BouncerCfg} = Clients,
|
||||
ok = application:set_env(
|
||||
?APP,
|
||||
service_clients,
|
||||
Clients#{Service => BouncerCfg#{url => Url}}
|
||||
).
|
||||
|
||||
mock_services_(Services, Config) when is_list(Config) ->
|
||||
mock_services_(Services, ?config(test_sup, Config));
|
||||
mock_services_(Services, SupPid) when is_pid(SupPid) ->
|
||||
Name = lists:map(fun get_service_name/1, Services),
|
||||
|
||||
Port = get_random_port(),
|
||||
{ok, IP} = inet:parse_address(?HOST_IP),
|
||||
ChildSpec = woody_server:child_spec(
|
||||
{dummy, Name},
|
||||
#{
|
||||
ip => IP,
|
||||
port => Port,
|
||||
event_handler => scoper_woody_event_handler,
|
||||
handlers => lists:map(fun mock_service_handler/1, Services)
|
||||
}
|
||||
),
|
||||
{ok, _} = supervisor:start_child(SupPid, ChildSpec),
|
||||
|
||||
lists:foldl(
|
||||
fun(Service, Acc) ->
|
||||
ServiceName = get_service_name(Service),
|
||||
Acc#{ServiceName => make_url(ServiceName, Port)}
|
||||
end,
|
||||
#{},
|
||||
Services
|
||||
).
|
||||
|
||||
get_service_name({ServiceName, _Fun}) ->
|
||||
ServiceName;
|
||||
get_service_name({ServiceName, _WoodyService, _Fun}) ->
|
||||
ServiceName.
|
||||
|
||||
mock_service_handler({ServiceName, Fun}) ->
|
||||
mock_service_handler(ServiceName, get_service_modname(ServiceName), Fun);
|
||||
mock_service_handler({ServiceName, WoodyService, Fun}) ->
|
||||
mock_service_handler(ServiceName, WoodyService, Fun).
|
||||
|
||||
mock_service_handler(ServiceName, WoodyService, Fun) ->
|
||||
{make_path(ServiceName), {WoodyService, {bouncer_client_mock_service, #{function => Fun}}}}.
|
||||
|
||||
get_service_modname(org_management) ->
|
||||
{orgmgmt_auth_context_provider_thrift, 'AuthContextProvider'};
|
||||
get_service_modname(bouncer) ->
|
||||
{bouncer_decisions_thrift, 'Arbiter'}.
|
||||
|
||||
% TODO not so failproof, ideally we need to bind socket first and then give to a ranch listener
|
||||
get_random_port() ->
|
||||
rand:uniform(32768) + 32767.
|
||||
|
||||
make_url(ServiceName, Port) ->
|
||||
iolist_to_binary(["http://", ?HOST_NAME, ":", integer_to_list(Port), make_path(ServiceName)]).
|
||||
|
||||
make_path(ServiceName) ->
|
||||
"/" ++ atom_to_list(ServiceName).
|
9
test/bouncer_client_mock_service.erl
Normal file
9
test/bouncer_client_mock_service.erl
Normal file
@ -0,0 +1,9 @@
|
||||
-module(bouncer_client_mock_service).
|
||||
|
||||
-behaviour(woody_server_thrift_handler).
|
||||
|
||||
-export([handle_function/4]).
|
||||
|
||||
-spec handle_function(woody:func(), woody:args(), woody_context:ctx(), #{}) -> {ok, term()}.
|
||||
handle_function(FunName, Args, _, #{function := Fun}) ->
|
||||
Fun(FunName, Args).
|
Loading…
Reference in New Issue
Block a user