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