diff --git a/src/bouncer_client.erl b/src/bouncer_client.erl index 02227fe..00c9745 100644 --- a/src/bouncer_client.erl +++ b/src/bouncer_client.erl @@ -2,9 +2,6 @@ -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 @@ -16,10 +13,9 @@ -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()} + bouncer_context_helpers:context_fragment() | {encoded_fragment, encoded_bouncer_fragment()}. -type judge_context() :: #{ @@ -74,11 +70,11 @@ collect_fragments(_, Context) -> collect_fragments_(FragmentID, {encoded_fragment, EncodedFragment}, Acc0) -> Acc0#{FragmentID => EncodedFragment}; -collect_fragments_(FragmentID, {fragment, Fragment}, Acc0) -> +collect_fragments_(FragmentID, ContextFragment = #bctx_v1_ContextFragment{}, Acc0) -> Acc0#{ FragmentID => #bctx_ContextFragment{ type = v1_thrift_binary, - content = encode_context_fragment(Fragment) + content = encode_context_fragment(ContextFragment) } }. diff --git a/src/bouncer_context_helpers.erl b/src/bouncer_context_helpers.erl index 426566a..bedb228 100644 --- a/src/bouncer_context_helpers.erl +++ b/src/bouncer_context_helpers.erl @@ -4,24 +4,28 @@ -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]). +-export([empty/0]). +-export([make_env_fragment/1]). +-export([add_env/2]). +-export([make_auth_fragment/1]). +-export([add_auth/2]). +-export([make_user_fragment/1]). +-export([add_user/2]). +-export([make_requester_fragment/1]). +-export([add_requester/2]). + +-export([get_user_orgs_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 context_fragment() :: bouncer_context_v1_thrift:'ContextFragment'(). -type woody_context() :: woody_context:ctx(). -type entity() :: #{ - id => id() + id := id() }. -type environment_params() :: #{ @@ -34,7 +38,7 @@ }. -type auth_params() :: #{ - method => method(), + method := method(), scope => [auth_scope()], expiration => timestamp() }. @@ -46,8 +50,8 @@ }. -type user_params() :: #{ - id => id(), - realm => entity(), + id := id(), + realm := entity(), email => email(), orgs => [user_org()] }. @@ -71,81 +75,84 @@ ip => ip() }. +-export_type([context_fragment/0]). + -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 empty() -> context_fragment(). +empty() -> + #bctx_v1_ContextFragment{}. --spec make_env_context_fragment(environment_params()) -> context_fragment(). -make_env_context_fragment(Params) -> - Datetime = maybe_get_param(now, Params), +-spec make_env_fragment(environment_params()) -> context_fragment(). +make_env_fragment(Params) -> + add_env(Params, empty()). + +-spec add_env(environment_params(), context_fragment()) -> context_fragment(). +add_env(Params, ContextFragment = #bctx_v1_ContextFragment{env = undefined}) -> + Now = maybe_get_param(now, Params, genlib_rfc3339:format(genlib_time:unow(), second)), Deployment = maybe_get_param(deployment, Params), DeploymentID = maybe_get_param(id, Deployment), - - {fragment, #bctx_v1_ContextFragment{ + ContextFragment#bctx_v1_ContextFragment{ env = #bctx_v1_Environment{ - now = Datetime, + now = Now, 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), +-spec make_auth_fragment(auth_params()) -> context_fragment(). +make_auth_fragment(Params) -> + add_auth(Params, empty()). + +-spec add_auth(auth_params(), context_fragment()) -> context_fragment(). +add_auth(Params, ContextFragment = #bctx_v1_ContextFragment{auth = undefined}) -> + Method = get_param(method, Params), Scope = maybe_get_param(scope, Params), Expiration = maybe_get_param(expiration, Params), - - {fragment, #bctx_v1_ContextFragment{ + ContextFragment#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_fragment(user_params()) -> context_fragment(). +make_user_fragment(Params) -> + add_user(Params, empty()). --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), +-spec add_user(user_params(), context_fragment()) -> context_fragment(). +add_user(Params, ContextFragment = #bctx_v1_ContextFragment{user = undefined}) -> + UserID = get_param(id, Params), + RealmEntity = get_param(realm, Params), Email = maybe_get_param(email, Params), Orgs = maybe_get_param(orgs, Params), - - {fragment, #bctx_v1_ContextFragment{ + ContextFragment#bctx_v1_ContextFragment{ user = #bctx_v1_User{ id = UserID, - realm = maybe_add_param(maybe_marshal_entity(RealmEntity), RealmEntity), + realm = marshal_entity(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) -> +-spec make_requester_fragment(requester_params()) -> context_fragment(). +make_requester_fragment(Params) -> + add_requester(Params, empty()). + +-spec add_requester(requester_params(), context_fragment()) -> context_fragment(). +add_requester(Params, ContextFragment = #bctx_v1_ContextFragment{requester = undefined}) -> IP = maybe_get_param(ip, Params), - - {fragment, #bctx_v1_ContextFragment{ + ContextFragment#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) -> +-spec get_user_orgs_fragment(id(), woody_context()) -> {ok, context_fragment()} | {error, {user, notfound}}. +get_user_orgs_fragment(UserID, WoodyContext) -> ServiceName = org_management, case bouncer_client_woody:call(ServiceName, 'GetUserContext', {UserID}, WoodyContext) of {ok, EncodedFragment} -> @@ -162,16 +169,28 @@ convert_fragment(org_management, {bctx_ContextFragment, Type = v1_thrift_binary, content = Content }. +get_param(Key, Map = #{}) -> + maps:get(Key, Map). + maybe_get_param(_Key, undefined) -> undefined; maybe_get_param(Key, Map) -> maps:get(Key, Map, undefined). +maybe_get_param(_Key, undefined, Default) -> + Default; +maybe_get_param(Key, Map, Default) -> + maps:get(Key, Map, Default). + maybe_add_param(_Value, undefined) -> undefined; maybe_add_param(Value, _Param) -> Value. +marshal_entity(Entity) -> + EntityID = get_param(id, Entity), + #bctx_v1_Entity{id = EntityID}. + maybe_marshal_entity(undefined) -> undefined; maybe_marshal_entity(Entity) -> diff --git a/test/bouncer_client_SUITE.erl b/test/bouncer_client_SUITE.erl index 8ee2ed9..f404a56 100644 --- a/test/bouncer_client_SUITE.erl +++ b/test/bouncer_client_SUITE.erl @@ -15,11 +15,11 @@ -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_complex_fragment/1]). -export([validate_remote_user_fragment/1]). -type test_case_name() :: atom(). @@ -39,11 +39,11 @@ groups() -> [ {default, [], [ empty_judge, - validate_default_user_fragment, validate_user_fragment, validate_env_fragment, validate_auth_fragment, validate_requester_fragment, + validate_complex_fragment, validate_remote_user_fragment ]} ]. @@ -105,37 +105,17 @@ empty_judge(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">>, + UserRealm = <<"external">>, mock_services( [ {bouncer, fun('Judge', {_RulesetID, Fragments}) -> - case get_user_id(Fragments) of - UserID -> + case get_fragment(<<"user">>, Fragments) of + #bctx_v1_ContextFragment{ + user = #bctx_v1_User{id = UserID, realm = #bctx_v1_Entity{id = UserRealm}} + } -> {ok, #bdcs_Judgement{resolution = allowed}}; _ -> {ok, #bdcs_Judgement{resolution = forbidden}} @@ -147,7 +127,14 @@ validate_user_fragment(C) -> WoodyContext = woody_context:new(), allowed = bouncer_client:judge( ?RULESET_ID, - #{fragments => #{<<"user">> => bouncer_context_helpers:make_user_context_fragment(#{id => UserID})}}, + #{ + fragments => #{ + <<"user">> => bouncer_context_helpers:make_user_fragment(#{ + id => UserID, + realm => #{id => UserRealm} + }) + } + }, WoodyContext ). @@ -170,7 +157,7 @@ validate_env_fragment(C) -> WoodyContext = woody_context:new(), allowed = bouncer_client:judge( ?RULESET_ID, - #{fragments => #{<<"env">> => bouncer_context_helpers:make_env_context_fragment(#{now => Time})}}, + #{fragments => #{<<"env">> => bouncer_context_helpers:make_env_fragment(#{now => Time})}}, WoodyContext ). @@ -193,7 +180,7 @@ validate_auth_fragment(C) -> WoodyContext = woody_context:new(), allowed = bouncer_client:judge( ?RULESET_ID, - #{fragments => #{<<"auth">> => bouncer_context_helpers:make_auth_context_fragment(#{method => Method})}}, + #{fragments => #{<<"auth">> => bouncer_context_helpers:make_auth_fragment(#{method => Method})}}, WoodyContext ). @@ -221,7 +208,48 @@ validate_requester_fragment(C) -> WoodyContext = woody_context:new(), allowed = bouncer_client:judge( ?RULESET_ID, - #{fragments => #{<<"requester">> => bouncer_context_helpers:make_requester_context_fragment(#{ip => IP})}}, + #{fragments => #{<<"requester">> => bouncer_context_helpers:make_requester_fragment(#{ip => IP})}}, + WoodyContext + ). + +-spec validate_complex_fragment(config()) -> _. +validate_complex_fragment(C) -> + mock_services( + [ + {bouncer, fun('Judge', {_RulesetID, Fragments}) -> + case Fragments of + #bdcs_Context{fragments = #{<<"complex">> := Fragment}} -> + case decode_fragment(Fragment) of + #bctx_v1_ContextFragment{ + env = #bctx_v1_Environment{}, + auth = #bctx_v1_Auth{}, + user = #bctx_v1_User{} + } -> + {ok, #bdcs_Judgement{resolution = allowed}}; + _ -> + {ok, #bdcs_Judgement{resolution = forbidden}} + end; + _ -> + {ok, #bdcs_Judgement{resolution = forbidden}} + end + end} + ], + C + ), + WoodyContext = woody_context:new(), + ComplexFragment = + bouncer_context_helpers:add_user( + #{id => <<"USER">>, realm => #{id => <<"external">>}, email => <<"user@example.org">>}, + bouncer_context_helpers:add_auth( + #{method => <<"METHOD">>}, + bouncer_context_helpers:make_env_fragment( + #{now => genlib_rfc3339:format(genlib_time:unow(), second)} + ) + ) + ), + allowed = bouncer_client:judge( + ?RULESET_ID, + #{fragments => #{<<"complex">> => ComplexFragment}}, WoodyContext ). @@ -250,20 +278,15 @@ validate_remote_user_fragment(C) -> C ), WoodyContext = woody_context:new(), - {ok, EncodedUserFragment} = bouncer_context_helpers:get_user_context_fragment(UserID, WoodyContext), + {ok, EncodedUserFragment} = bouncer_context_helpers:get_user_orgs_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 - } - } + fragments = #{<<"requester">> := Fragment} }) -> - case decode(Fragment) of + case decode_fragment(Fragment) of {error, _} = Error -> error(Error); #bctx_v1_ContextFragment{requester = #bctx_v1_Requester{ip = IP}} -> @@ -271,14 +294,9 @@ get_ip(#bdcs_Context{ end. get_auth_method(#bdcs_Context{ - fragments = #{ - <<"auth">> := #bctx_ContextFragment{ - type = v1_thrift_binary, - content = Fragment - } - } + fragments = #{<<"auth">> := Fragment} }) -> - case decode(Fragment) of + case decode_fragment(Fragment) of {error, _} = Error -> error(Error); #bctx_v1_ContextFragment{auth = #bctx_v1_Auth{method = Method}} -> @@ -286,14 +304,9 @@ get_auth_method(#bdcs_Context{ end. get_time(#bdcs_Context{ - fragments = #{ - <<"env">> := #bctx_ContextFragment{ - type = v1_thrift_binary, - content = Fragment - } - } + fragments = #{<<"env">> := Fragment} }) -> - case decode(Fragment) of + case decode_fragment(Fragment) of {error, _} = Error -> error(Error); #bctx_v1_ContextFragment{env = #bctx_v1_Environment{now = Time}} -> @@ -301,21 +314,29 @@ get_time(#bdcs_Context{ end. get_user_id(#bdcs_Context{ - fragments = #{ - <<"user">> := #bctx_ContextFragment{ - type = v1_thrift_binary, - content = Fragment - } - } + fragments = #{<<"user">> := Fragment} }) -> - case decode(Fragment) of + case decode_fragment(Fragment) of {error, _} = Error -> error(Error); #bctx_v1_ContextFragment{user = #bctx_v1_User{id = UserID}} -> UserID end. -decode(Content) -> +get_fragment(ID, #bdcs_Context{ + fragments = Fragments +}) -> + case decode_fragment(maps:get(ID, Fragments)) of + {error, _} = Error -> + error(Error); + Fragment = #bctx_v1_ContextFragment{} -> + Fragment + end. + +decode_fragment(#bctx_ContextFragment{type = v1_thrift_binary, content = Content}) -> + decode_fragment_content(Content). + +decode_fragment_content(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