Add helpers API to combine parts of context (#2)

Motivation here: conceptually bouncer should see a single versioned
fragment for a single context producer. So a library user should be able
to combine multiple features in a single fragment.

Also remove _default_ fragment constructors, since it seems risky to
imply there are any defaults to begin with.

Also simplify naming a bit, so there're less tautologies.

* Drop unrelated includes
* Enforce some most sensitive bits of context
* Fix formatting
This commit is contained in:
Andrew Mayorov 2020-11-30 18:52:26 +03:00 committed by GitHub
parent 2143a16bde
commit 41f766feba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 157 additions and 121 deletions

View File

@ -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)
}
}.

View File

@ -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) ->

View File

@ -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