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:
Артем 2020-11-18 13:41:20 +03:00 committed by GitHub
parent 878de0942d
commit d053efc9a3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 1187 additions and 0 deletions

22
Jenkinsfile vendored Normal file
View 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
View 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

3
README.md Normal file
View File

@ -0,0 +1,3 @@
# Bouncer client erlang
Клиент для сервиса Bouncer

1
build_utils Submodule

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

55
elvis.config Normal file
View 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
View 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
View 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">>}]}
].

View 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
View 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.

View 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).

View 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).

View 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).

View 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).