mirror of
https://github.com/valitydev/token-keeper.git
synced 2024-11-06 02:15:21 +00:00
ED-192: Add invoice_template_access_token extractor (#10)
This commit is contained in:
parent
aaf2428ab3
commit
81fb238b29
@ -57,20 +57,26 @@
|
||||
id => <<"test.rbkmoney.keycloak">>,
|
||||
%% Where to fetch authdata for tokens issued by this authority
|
||||
authdata_sources => [
|
||||
%% Fetch from service storage
|
||||
%% Fetch from service storage (currently NOT IMPLEMENTED)
|
||||
storage,
|
||||
%% Extract bouncer context from the token itself and make up the rest
|
||||
{extract, #{
|
||||
%% Configuration for how to extract said context
|
||||
methods => [
|
||||
%% Extract bouncer context from claim
|
||||
claim,
|
||||
{claim, #{
|
||||
metadata_ns => <<"test.rbkmoney.apikeymgmt">>
|
||||
}},
|
||||
%% Create bouncer context from various legacy data the token has to offer
|
||||
%% Avaiable options are: `phony_api_key`, `user_session_token`, `detect_token`
|
||||
%% `user_session_token` requires the `user_realm` option to be set
|
||||
%% `detect_token` tries to determine wether the token is an
|
||||
%% `phony_api_key` or `user_session_token` based on token's source context and
|
||||
%% `user_session_token_origins` option
|
||||
%% Avaiable options are: `phony_api_key`, `user_session_token`,
|
||||
%% `invoice_template_access_token`, `detect_token`
|
||||
%% - `user_session_token` requires `user_realm` option to be set
|
||||
%% - `invoice_template_access_token` requires
|
||||
%% `domain` option to be set (refer to legacy uac auth options)
|
||||
%% - `detect_token` tries to determine wether the token is an
|
||||
%% `phony_api_key` or `user_session_token` based on token's source context and
|
||||
%% `user_session_token_origins` option
|
||||
%% ALL extractor types require `metadata_ns` to be set
|
||||
{detect_token, #{
|
||||
%% phony_api_key options to use (can be used standalone)
|
||||
phony_api_key_opts => #{
|
||||
|
@ -6,17 +6,11 @@
|
||||
|
||||
%% API functions
|
||||
|
||||
-export([get_authdata/3]).
|
||||
-export([get_authdata/2]).
|
||||
|
||||
%% API Types
|
||||
|
||||
-type authdata_source() :: token_source() | {token_source(), source_opts()}.
|
||||
|
||||
-type token_source() :: storage | extractor.
|
||||
|
||||
-type source_opts() :: #{
|
||||
methods => tk_context_extractor:methods()
|
||||
}.
|
||||
-type authdata_source() :: storage_source() | extractor_source().
|
||||
|
||||
-type stored_authdata() :: #{
|
||||
id => tk_authority:id(),
|
||||
@ -26,19 +20,33 @@
|
||||
}.
|
||||
|
||||
-export_type([authdata_source/0]).
|
||||
-export_type([token_source/0]).
|
||||
-export_type([source_opts/0]).
|
||||
-export_type([stored_authdata/0]).
|
||||
|
||||
%% Internal types
|
||||
|
||||
-type storage_source() :: maybe_opts(storage, tk_authdata_source_storage:source_opts()).
|
||||
-type extractor_source() :: maybe_opts(extractor, tk_authdata_source_extractor:source_opts()).
|
||||
-type maybe_opts(Source, Opts) :: Source | {Source, Opts}.
|
||||
|
||||
-type source_opts() ::
|
||||
tk_authdata_source_extractor:source_opts()
|
||||
| tk_authdata_source_storage:source_opts().
|
||||
|
||||
%% API functions
|
||||
|
||||
-spec get_authdata(token_source(), tk_token_jwt:t(), source_opts()) -> stored_authdata() | undefined.
|
||||
get_authdata(Source, Token, Opts) ->
|
||||
-spec get_authdata(authdata_source(), tk_token_jwt:t()) -> stored_authdata() | undefined.
|
||||
get_authdata(AuthDataSource, Token) ->
|
||||
{Source, Opts} = get_source_opts(AuthDataSource),
|
||||
Hander = get_source_handler(Source),
|
||||
Hander:get_authdata(Token, Opts).
|
||||
|
||||
%%
|
||||
|
||||
get_source_opts({_Source, _Opts} = SourceOpts) ->
|
||||
SourceOpts;
|
||||
get_source_opts(Source) when is_atom(Source) ->
|
||||
{Source, #{}}.
|
||||
|
||||
get_source_handler(storage) ->
|
||||
tk_authdata_source_storage;
|
||||
get_source_handler(extract) ->
|
||||
|
@ -7,10 +7,16 @@
|
||||
|
||||
-export([get_authdata/2]).
|
||||
|
||||
%%
|
||||
|
||||
-type source_opts() :: #{
|
||||
methods => tk_context_extractor:methods()
|
||||
}.
|
||||
-export_type([source_opts/0]).
|
||||
|
||||
%% Behaviour functions
|
||||
|
||||
-spec get_authdata(tk_token_jwt:t(), tk_authdata_source:source_opts()) ->
|
||||
tk_authdata_source:stored_authdata() | undefined.
|
||||
-spec get_authdata(tk_token_jwt:t(), source_opts()) -> tk_authdata_source:stored_authdata() | undefined.
|
||||
get_authdata(Token, Opts) ->
|
||||
Methods = get_extractor_methods(Opts),
|
||||
case extract_context_with(Methods, Token) of
|
||||
|
@ -5,9 +5,14 @@
|
||||
|
||||
-export([get_authdata/2]).
|
||||
|
||||
%%
|
||||
|
||||
-type source_opts() :: #{}.
|
||||
-export_type([source_opts/0]).
|
||||
|
||||
%% Behaviour functions
|
||||
|
||||
-spec get_authdata(tk_token_jwt:t(), tk_authdata_source:source_opts()) -> undefined.
|
||||
-spec get_authdata(tk_token_jwt:t(), source_opts()) -> undefined.
|
||||
get_authdata(_Token, _Opts) ->
|
||||
%@TODO: This is for when we actually have storage for authdata
|
||||
undefined.
|
||||
|
@ -65,8 +65,7 @@ get_auth_data_sources(Authority) ->
|
||||
get_authdata_from_sources([], _Token) ->
|
||||
undefined;
|
||||
get_authdata_from_sources([SourceOpts | Rest], Token) ->
|
||||
{Source, Opts} = get_source_opts(SourceOpts),
|
||||
case tk_authdata_source:get_authdata(Source, Token, Opts) of
|
||||
case tk_authdata_source:get_authdata(SourceOpts, Token) of
|
||||
AuthData when AuthData =/= undefined ->
|
||||
AuthData;
|
||||
undefined ->
|
||||
@ -75,8 +74,3 @@ get_authdata_from_sources([SourceOpts | Rest], Token) ->
|
||||
|
||||
add_authority_id(AuthData, Authority) ->
|
||||
AuthData#{authority => maps:get(id, Authority)}.
|
||||
|
||||
get_source_opts({_Source, _Opts} = SourceOpts) ->
|
||||
SourceOpts;
|
||||
get_source_opts(Source) when is_atom(Source) ->
|
||||
{Source, #{}}.
|
||||
|
@ -11,13 +11,14 @@
|
||||
%% API Types
|
||||
|
||||
-type methods() :: [{method(), extractor_opts()} | method()].
|
||||
-type method() :: claim | detect_token | api_key_token | user_session_token.
|
||||
-type method() :: claim | detect_token | api_key_token | user_session_token | invoice_template_access_token.
|
||||
|
||||
-type extractor_opts() ::
|
||||
tk_extractor_claim:extractor_opts()
|
||||
| tk_extractor_detect_token:extractor_opts()
|
||||
| tk_extractor_phony_api_key:extractor_opts()
|
||||
| tk_extractor_user_session_token:extractor_opts().
|
||||
| tk_extractor_user_session_token:extractor_opts()
|
||||
| tk_extractor_invoice_tpl_token:extractor_opts().
|
||||
|
||||
-type extracted_context() :: {context_fragment(), tk_authority:metadata() | undefined}.
|
||||
|
||||
@ -49,4 +50,6 @@ get_extractor_handler(detect_token) ->
|
||||
get_extractor_handler(phony_api_key) ->
|
||||
tk_extractor_phony_api_key;
|
||||
get_extractor_handler(user_session_token) ->
|
||||
tk_extractor_user_session_token.
|
||||
tk_extractor_user_session_token;
|
||||
get_extractor_handler(invoice_template_access_token) ->
|
||||
tk_extractor_invoice_tpl_token.
|
||||
|
@ -20,8 +20,7 @@ get_context(Token, ExtractorOpts) ->
|
||||
% TODO
|
||||
% We deliberately do not handle decoding errors here since we extract claims from verified
|
||||
% tokens only, hence they must be well-formed here.
|
||||
Claims = tk_token_jwt:get_claims(Token),
|
||||
case get_claim(Claims) of
|
||||
case get_bouncer_claim(Token) of
|
||||
{ok, ClaimFragment} ->
|
||||
{ClaimFragment, wrap_metadata(get_metadata(Token), ExtractorOpts)};
|
||||
undefined ->
|
||||
@ -31,21 +30,25 @@ get_context(Token, ExtractorOpts) ->
|
||||
%% Internal functions
|
||||
|
||||
get_metadata(Token) ->
|
||||
Metadata = maps:with(get_passthrough_claim_names(), tk_token_jwt:get_claims(Token)),
|
||||
%% @TEMP: This is a temporary hack.
|
||||
%% When some external services will stop requiring woody user identity to be present it must be removed too
|
||||
case tk_token_jwt:get_subject_id(Token) of
|
||||
UserID when UserID =/= undefined ->
|
||||
#{<<"party_id">> => UserID};
|
||||
undefined ->
|
||||
undefined
|
||||
end.
|
||||
genlib_map:compact(Metadata#{
|
||||
<<"party_id">> => tk_token_jwt:get_subject_id(Token)
|
||||
}).
|
||||
|
||||
wrap_metadata(undefined, _ExtractorOpts) ->
|
||||
wrap_metadata(Metadata, _ExtractorOpts) when map_size(Metadata) =:= 0 ->
|
||||
undefined;
|
||||
wrap_metadata(Metadata, ExtractorOpts) ->
|
||||
MetadataNS = maps:get(metadata_ns, ExtractorOpts),
|
||||
#{MetadataNS => Metadata}.
|
||||
|
||||
get_passthrough_claim_names() ->
|
||||
[
|
||||
%% token consumer
|
||||
<<"cons">>
|
||||
].
|
||||
|
||||
-define(CLAIM_BOUNCER_CTX, <<"bouncer_ctx">>).
|
||||
-define(CLAIM_CTX_TYPE, <<"ty">>).
|
||||
-define(CLAIM_CTX_CONTEXT, <<"ct">>).
|
||||
@ -53,23 +56,22 @@ wrap_metadata(Metadata, ExtractorOpts) ->
|
||||
-define(CLAIM_CTX_TYPE_V1_THRIFT_BINARY, <<"v1_thrift_binary">>).
|
||||
|
||||
-type claim() :: tk_token_jwt:claim().
|
||||
-type claims() :: tk_token_jwt:claims().
|
||||
|
||||
-spec get_claim(claims()) ->
|
||||
-spec get_bouncer_claim(tk_token_jwt:t()) ->
|
||||
{ok, tk_context_extractor:context_fragment()}
|
||||
| {error, {unsupported, claim()} | {malformed, binary()}}
|
||||
| undefined.
|
||||
get_claim(Claims) ->
|
||||
case maps:get(?CLAIM_BOUNCER_CTX, Claims, undefined) of
|
||||
get_bouncer_claim(Token) ->
|
||||
case tk_token_jwt:get_claim(?CLAIM_BOUNCER_CTX, Token, undefined) of
|
||||
Claim when Claim /= undefined ->
|
||||
decode_claim(Claim);
|
||||
decode_bouncer_claim(Claim);
|
||||
undefined ->
|
||||
undefined
|
||||
end.
|
||||
|
||||
-spec decode_claim(claim()) ->
|
||||
-spec decode_bouncer_claim(claim()) ->
|
||||
{ok, tk_context_extractor:context_fragment()} | {error, {unsupported, claim()} | {malformed, binary()}}.
|
||||
decode_claim(#{
|
||||
decode_bouncer_claim(#{
|
||||
?CLAIM_CTX_TYPE := ?CLAIM_CTX_TYPE_V1_THRIFT_BINARY,
|
||||
?CLAIM_CTX_CONTEXT := Content
|
||||
}) ->
|
||||
@ -85,5 +87,5 @@ decode_claim(#{
|
||||
error:_ ->
|
||||
{error, {malformed, Content}}
|
||||
end;
|
||||
decode_claim(Ctx) ->
|
||||
decode_bouncer_claim(Ctx) ->
|
||||
{error, {unsupported, Ctx}}.
|
||||
|
175
src/tk_extractor_invoice_tpl_token.erl
Normal file
175
src/tk_extractor_invoice_tpl_token.erl
Normal file
@ -0,0 +1,175 @@
|
||||
-module(tk_extractor_invoice_tpl_token).
|
||||
|
||||
%% NOTE:
|
||||
%% This is here because of a historical decision to make InvoiceTemplateAccessToken(s) never expire,
|
||||
%% therefore a lot of them do not have a standart bouncer context claim built-in.
|
||||
%% It is advisable to get rid of this exctractor when this issue will be solved.
|
||||
|
||||
-behaviour(tk_context_extractor).
|
||||
|
||||
-export([get_context/2]).
|
||||
|
||||
%%
|
||||
|
||||
-type extractor_opts() :: #{
|
||||
domain := binary(),
|
||||
metadata_ns := binary()
|
||||
}.
|
||||
|
||||
-export_type([extractor_opts/0]).
|
||||
|
||||
%% API functions
|
||||
|
||||
-spec get_context(tk_token_jwt:t(), extractor_opts()) -> tk_context_extractor:extracted_context().
|
||||
get_context(Token, ExtractorOpts) ->
|
||||
UserID = tk_token_jwt:get_subject_id(Token),
|
||||
case extract_invoice_template_rights(Token, ExtractorOpts) of
|
||||
{ok, InvoiceTemplateID} ->
|
||||
BCtx = create_bouncer_ctx(tk_token_jwt:get_token_id(Token), UserID, InvoiceTemplateID),
|
||||
{BCtx, wrap_metadata(get_metadata(Token), ExtractorOpts)};
|
||||
{error, Reason} ->
|
||||
_ = logger:warning("Failed to extract invoice template rights: ~p", [Reason]),
|
||||
undefined
|
||||
end.
|
||||
|
||||
%%
|
||||
|
||||
get_metadata(Token) ->
|
||||
%% @TEMP: This is a temporary hack.
|
||||
%% When some external services will stop requiring woody user identity to be present it must be removed too
|
||||
case tk_token_jwt:get_subject_id(Token) of
|
||||
UserID when UserID =/= undefined ->
|
||||
#{<<"party_id">> => UserID};
|
||||
undefined ->
|
||||
undefined
|
||||
end.
|
||||
|
||||
extract_invoice_template_rights(TokenContext, ExtractorOpts) ->
|
||||
Domain = maps:get(domain, ExtractorOpts),
|
||||
case get_acl(Domain, get_resource_hierarchy(), TokenContext) of
|
||||
{ok, TokenACL} ->
|
||||
match_invoice_template_acl(TokenACL);
|
||||
{error, Reason} ->
|
||||
{error, {acl, Reason}}
|
||||
end.
|
||||
|
||||
match_invoice_template_acl(TokenACL) ->
|
||||
Patterns = [
|
||||
fun({[party, {invoice_templates, ID}], [read]}) -> ID end,
|
||||
fun({[party, {invoice_templates, ID}, invoice_template_invoices], [write]}) -> ID end
|
||||
],
|
||||
case match_acl(Patterns, TokenACL) of
|
||||
[[InvoiceTemplateID], [InvoiceTemplateID]] ->
|
||||
{ok, InvoiceTemplateID};
|
||||
Matches ->
|
||||
{error, {acl_mismatch, Matches}}
|
||||
end.
|
||||
|
||||
match_acl(Patterns, TokenACL) ->
|
||||
[match_acl_pattern(TokenACL, Pat) || Pat <- Patterns].
|
||||
|
||||
match_acl_pattern(TokenACL, Pat) ->
|
||||
lists:usort([Match || Entry <- TokenACL, Match <- run_pattern(Entry, Pat)]).
|
||||
|
||||
run_pattern(Entry, Pat) when is_function(Pat, 1) ->
|
||||
try
|
||||
[Pat(Entry)]
|
||||
catch
|
||||
error:function_clause -> []
|
||||
end.
|
||||
|
||||
get_acl(Domain, Hierarchy, TokenContext) ->
|
||||
case tk_token_jwt:get_claim(<<"resource_access">>, TokenContext, undefined) of
|
||||
#{Domain := #{<<"roles">> := Roles}} ->
|
||||
try
|
||||
TokenACL = tk_token_legacy_acl:decode(Roles, Hierarchy),
|
||||
{ok, tk_token_legacy_acl:to_list(TokenACL)}
|
||||
catch
|
||||
error:Reason -> {error, {invalid, Reason}}
|
||||
end;
|
||||
_ ->
|
||||
{error, missing}
|
||||
end.
|
||||
|
||||
create_bouncer_ctx(TokenID, UserID, InvoiceTemplateID) ->
|
||||
bouncer_context_helpers:add_auth(
|
||||
#{
|
||||
method => <<"InvoiceTemplateAccessToken">>,
|
||||
token => #{id => TokenID},
|
||||
scope => [
|
||||
#{
|
||||
party => #{id => UserID},
|
||||
invoice_template => #{id => InvoiceTemplateID}
|
||||
}
|
||||
]
|
||||
},
|
||||
bouncer_context_helpers:empty()
|
||||
).
|
||||
|
||||
wrap_metadata(Metadata, ExtractorOpts) ->
|
||||
MetadataNS = maps:get(metadata_ns, ExtractorOpts),
|
||||
#{MetadataNS => Metadata}.
|
||||
|
||||
get_resource_hierarchy() ->
|
||||
#{
|
||||
party => #{invoice_templates => #{invoice_template_invoices => #{}}}
|
||||
}.
|
||||
|
||||
-ifdef(TEST).
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
|
||||
-spec test() -> _.
|
||||
|
||||
-define(TEST_ACL, [
|
||||
{some_other_stuff, 123, <<"abc">>},
|
||||
{second, <<"abc">>},
|
||||
{doubles, 123},
|
||||
more_stuff,
|
||||
{test_acl, 123},
|
||||
{doubles, 456},
|
||||
{first, 123}
|
||||
]).
|
||||
|
||||
-spec match_acl_base_test() -> _.
|
||||
|
||||
match_acl_base_test() ->
|
||||
[[123]] = match_acl(
|
||||
[
|
||||
fun({test_acl, Int}) -> Int end
|
||||
],
|
||||
?TEST_ACL
|
||||
).
|
||||
|
||||
-spec match_acl_dupes_test() -> _.
|
||||
|
||||
match_acl_dupes_test() ->
|
||||
[[123, 456]] = match_acl(
|
||||
[
|
||||
fun({doubles, Int}) -> Int end
|
||||
],
|
||||
?TEST_ACL
|
||||
).
|
||||
|
||||
-spec match_acl_order_test() -> _.
|
||||
|
||||
match_acl_order_test() ->
|
||||
[[123], [<<"abc">>]] = match_acl(
|
||||
[
|
||||
fun({first, Int}) -> Int end,
|
||||
fun({second, Bin}) -> Bin end
|
||||
],
|
||||
?TEST_ACL
|
||||
).
|
||||
|
||||
-spec match_acl_no_match_test() -> _.
|
||||
|
||||
match_acl_no_match_test() ->
|
||||
[[], []] = match_acl(
|
||||
[
|
||||
fun({foo, _}) -> wait end,
|
||||
fun({bar, _, _}) -> no end
|
||||
],
|
||||
?TEST_ACL
|
||||
).
|
||||
|
||||
-endif.
|
@ -19,12 +19,13 @@ get_context(Token, ExtractorOpts) ->
|
||||
UserID = tk_token_jwt:get_subject_id(Token),
|
||||
Email = tk_token_jwt:get_subject_email(Token),
|
||||
Expiration = tk_token_jwt:get_expires_at(Token),
|
||||
UserRealm = maps:get(user_realm, ExtractorOpts, undefined),
|
||||
Acc0 = bouncer_context_helpers:empty(),
|
||||
Acc1 = bouncer_context_helpers:add_user(
|
||||
#{
|
||||
id => UserID,
|
||||
email => Email,
|
||||
realm => #{id => maps:get(user_realm, ExtractorOpts, undefined)}
|
||||
realm => #{id => UserRealm}
|
||||
},
|
||||
Acc0
|
||||
),
|
||||
@ -40,7 +41,8 @@ get_context(Token, ExtractorOpts) ->
|
||||
wrap_metadata(
|
||||
genlib_map:compact(#{
|
||||
<<"user_id">> => UserID,
|
||||
<<"user_email">> => Email
|
||||
<<"user_email">> => Email,
|
||||
<<"user_realm">> => UserRealm
|
||||
}),
|
||||
ExtractorOpts
|
||||
)}.
|
||||
|
141
src/tk_token_legacy_acl.erl
Normal file
141
src/tk_token_legacy_acl.erl
Normal file
@ -0,0 +1,141 @@
|
||||
-module(tk_token_legacy_acl).
|
||||
|
||||
%%
|
||||
|
||||
-opaque t() :: [{{priority(), scope()}, [permission()]}].
|
||||
|
||||
-type priority() :: integer().
|
||||
-type unknown_scope() :: {unknown, binary()}.
|
||||
-type known_scope() :: [resource() | {resource(), resource_id()}, ...].
|
||||
-type scope() :: known_scope() | unknown_scope().
|
||||
-type resource() :: atom().
|
||||
-type resource_id() :: binary().
|
||||
-type permission() :: read | write.
|
||||
-type resource_hierarchy() :: map().
|
||||
|
||||
-export_type([t/0]).
|
||||
-export_type([scope/0]).
|
||||
-export_type([known_scope/0]).
|
||||
-export_type([resource/0]).
|
||||
-export_type([permission/0]).
|
||||
-export_type([resource_hierarchy/0]).
|
||||
|
||||
-export([to_list/1]).
|
||||
-export([decode/2]).
|
||||
|
||||
%%
|
||||
|
||||
-spec to_list(t()) -> [{scope(), [permission()]}].
|
||||
to_list(ACL) ->
|
||||
[{S, P} || {{_, S}, P} <- ACL].
|
||||
|
||||
%%
|
||||
|
||||
-spec decode([binary()], resource_hierarchy()) -> t().
|
||||
decode(BinaryACL, ResourceHierarchy) ->
|
||||
lists:foldl(
|
||||
fun(V, ACL) ->
|
||||
decode_entry(V, ACL, ResourceHierarchy)
|
||||
end,
|
||||
[],
|
||||
BinaryACL
|
||||
).
|
||||
|
||||
decode_entry(V, ACL, ResourceHierarchy) ->
|
||||
case binary:split(V, <<":">>, [global]) of
|
||||
[V1, V2] ->
|
||||
Scope = decode_scope(V1, ResourceHierarchy),
|
||||
Permission = decode_permission(V2),
|
||||
insert_scope(Scope, Permission, ACL, ResourceHierarchy);
|
||||
_ ->
|
||||
error({badarg, {role, V}})
|
||||
end.
|
||||
|
||||
decode_scope(V, ResourceHierarchy) ->
|
||||
try
|
||||
decode_scope_frags(binary:split(V, <<".">>, [global]), ResourceHierarchy)
|
||||
catch
|
||||
error:{badarg, _} ->
|
||||
{unknown, V}
|
||||
end.
|
||||
|
||||
decode_scope_frags([V1, V2 | Vs], H) ->
|
||||
{Resource, H1} = decode_scope_frag_resource(V1, V2, H),
|
||||
[Resource | decode_scope_frags(Vs, H1)];
|
||||
decode_scope_frags([V], H) ->
|
||||
decode_scope_frags([V, <<"*">>], H);
|
||||
decode_scope_frags([], _) ->
|
||||
[].
|
||||
|
||||
decode_scope_frag_resource(V, <<"*">>, H) ->
|
||||
R = decode_resource(V),
|
||||
{R, delve(R, H)};
|
||||
decode_scope_frag_resource(V, ID, H) ->
|
||||
R = decode_resource(V),
|
||||
{{R, ID}, delve(R, H)}.
|
||||
|
||||
decode_resource(V) ->
|
||||
try
|
||||
binary_to_existing_atom(V, utf8)
|
||||
catch
|
||||
error:badarg ->
|
||||
error({badarg, {resource, V}})
|
||||
end.
|
||||
|
||||
decode_permission(<<"read">>) ->
|
||||
read;
|
||||
decode_permission(<<"write">>) ->
|
||||
write;
|
||||
decode_permission(V) ->
|
||||
error({badarg, {permission, V}}).
|
||||
|
||||
%%
|
||||
|
||||
-spec insert_scope(scope(), permission(), t(), resource_hierarchy()) -> t().
|
||||
insert_scope({unknown, _} = Scope, Permission, ACL, _ResourceHierarchy) ->
|
||||
insert({{0, Scope}, [Permission]}, ACL);
|
||||
insert_scope(Scope, Permission, ACL, ResourceHierarchy) ->
|
||||
Priority = compute_priority(Scope, ResourceHierarchy),
|
||||
insert({{Priority, Scope}, [Permission]}, ACL).
|
||||
|
||||
insert({PS, _} = V, [{PS0, _} = V0 | Vs]) when PS < PS0 ->
|
||||
[V0 | insert(V, Vs)];
|
||||
insert({PS, Perms}, [{PS, Perms0} | Vs]) ->
|
||||
% NOTE squashing permissions of entries with the same scope
|
||||
[{PS, lists:usort(Perms ++ Perms0)} | Vs];
|
||||
insert({PS, _} = V, [{PS0, _} | _] = Vs) when PS > PS0 ->
|
||||
[V | Vs];
|
||||
insert(V, []) ->
|
||||
[V].
|
||||
|
||||
%%
|
||||
|
||||
compute_priority(Scope, ResourceHierarchy) ->
|
||||
% NOTE
|
||||
% Scope priority depends on the following attributes, in the order of decreasing
|
||||
% importance:
|
||||
% 1. Depth, deeper is more important
|
||||
% 2. Scope element specificity, element marked with an ID is more important
|
||||
compute_scope_priority(Scope, ResourceHierarchy).
|
||||
|
||||
compute_scope_priority(Scope, ResourceHierarchy) when length(Scope) > 0 ->
|
||||
compute_scope_priority(Scope, ResourceHierarchy, 0);
|
||||
compute_scope_priority(Scope, _ResourceHierarchy) ->
|
||||
error({badarg, {scope, Scope}}).
|
||||
|
||||
compute_scope_priority([{Resource, _ID} | Rest], H, P) ->
|
||||
compute_scope_priority(Rest, delve(Resource, H), P * 10 + 2);
|
||||
compute_scope_priority([Resource | Rest], H, P) ->
|
||||
compute_scope_priority(Rest, delve(Resource, H), P * 10 + 1);
|
||||
compute_scope_priority([], _, P) ->
|
||||
P * 10.
|
||||
|
||||
%%
|
||||
|
||||
delve(Resource, Hierarchy) ->
|
||||
case maps:find(Resource, Hierarchy) of
|
||||
{ok, Sub} ->
|
||||
Sub;
|
||||
error ->
|
||||
error({badarg, {resource, Resource}})
|
||||
end.
|
@ -23,6 +23,10 @@
|
||||
-export([detect_dummy_token_test/1]).
|
||||
-export([no_token_claim_test/1]).
|
||||
-export([bouncer_context_from_claims_test/1]).
|
||||
-export([cons_claim_passthrough_test/1]).
|
||||
-export([invoice_template_access_token_ok_test/1]).
|
||||
-export([invoice_template_access_token_no_access_test/1]).
|
||||
-export([invoice_template_access_token_invalid_access_test/1]).
|
||||
|
||||
-type config() :: ct_helper:config().
|
||||
-type group_name() :: atom().
|
||||
@ -36,6 +40,8 @@
|
||||
-define(TK_AUTHORITY_KEYCLOAK, <<"test.rbkmoney.keycloak">>).
|
||||
-define(TK_AUTHORITY_CAPI, <<"test.rbkmoney.capi">>).
|
||||
|
||||
-define(TK_RESOURCE_DOMAIN, <<"test-domain">>).
|
||||
|
||||
-define(METADATA(Authority, Metadata), #{Authority := Metadata}).
|
||||
-define(PARTY_METADATA(Authority, SubjectID), ?METADATA(Authority, #{<<"party_id">> := SubjectID})).
|
||||
-define(USER_METADATA(Authority, SubjectID, Email),
|
||||
@ -56,7 +62,8 @@
|
||||
all() ->
|
||||
[
|
||||
{group, detect_token_type},
|
||||
{group, claim_only}
|
||||
{group, claim_only},
|
||||
{group, invoice_template_access_token}
|
||||
].
|
||||
|
||||
-spec groups() -> [{group_name(), list(), [test_case_name()]}].
|
||||
@ -69,7 +76,13 @@ groups() ->
|
||||
]},
|
||||
{claim_only, [parallel], [
|
||||
no_token_claim_test,
|
||||
bouncer_context_from_claims_test
|
||||
bouncer_context_from_claims_test,
|
||||
cons_claim_passthrough_test
|
||||
]},
|
||||
{invoice_template_access_token, [parallel], [
|
||||
invoice_template_access_token_ok_test,
|
||||
invoice_template_access_token_no_access_test,
|
||||
invoice_template_access_token_invalid_access_test
|
||||
]}
|
||||
].
|
||||
|
||||
@ -148,17 +161,42 @@ init_per_group(claim_only = Name, C) ->
|
||||
}}
|
||||
]) ++
|
||||
[{groupname, Name} | C];
|
||||
init_per_group(invoice_template_access_token = Name, C) ->
|
||||
start_keeper([
|
||||
{jwt, #{
|
||||
keyset => #{
|
||||
test => #{
|
||||
source => {pem_file, get_keysource("keys/local/private.pem", C)},
|
||||
authority => invoice_tpl_authority
|
||||
}
|
||||
}
|
||||
}},
|
||||
{authorities, #{
|
||||
invoice_tpl_authority => #{
|
||||
id => ?TK_AUTHORITY_CAPI,
|
||||
authdata_sources => [
|
||||
{extract, #{
|
||||
methods => [
|
||||
{claim, #{
|
||||
metadata_ns => ?TK_META_NS_APIKEYMGMT
|
||||
}},
|
||||
{invoice_template_access_token, #{
|
||||
domain => ?TK_RESOURCE_DOMAIN,
|
||||
metadata_ns => ?TK_META_NS_APIKEYMGMT
|
||||
}}
|
||||
]
|
||||
}}
|
||||
]
|
||||
}
|
||||
}}
|
||||
]) ++
|
||||
[{groupname, Name} | C];
|
||||
init_per_group(Name, C) ->
|
||||
[{groupname, Name} | C].
|
||||
|
||||
-spec end_per_group(group_name(), config()) -> _.
|
||||
end_per_group(GroupName, C) when
|
||||
GroupName =:= detect_token_type;
|
||||
GroupName =:= claim_only
|
||||
->
|
||||
end_per_group(_GroupName, C) ->
|
||||
ok = stop_keeper(C),
|
||||
ok;
|
||||
end_per_group(_Name, _C) ->
|
||||
ok.
|
||||
|
||||
-spec init_per_testcase(atom(), config()) -> config().
|
||||
@ -206,14 +244,15 @@ detect_api_key_test(C) ->
|
||||
JTI = unique_id(),
|
||||
SubjectID = <<"TEST">>,
|
||||
{ok, Token} = issue_token(JTI, #{<<"sub">> => SubjectID}, unlimited),
|
||||
AuthData = call_get_by_token(Token, ?TOKEN_SOURCE_CONTEXT(), Client),
|
||||
_ = ct:pal("~p", [AuthData]),
|
||||
?assertEqual(undefined, AuthData#token_keeper_AuthData.id),
|
||||
?assertEqual(Token, AuthData#token_keeper_AuthData.token),
|
||||
?assertEqual(active, AuthData#token_keeper_AuthData.status),
|
||||
?assert(assert_context({api_key_token, JTI, SubjectID}, AuthData#token_keeper_AuthData.context)),
|
||||
?assertMatch(?PARTY_METADATA(?TK_META_NS_APIKEYMGMT, SubjectID), AuthData#token_keeper_AuthData.metadata),
|
||||
?assertEqual(?TK_AUTHORITY_KEYCLOAK, AuthData#token_keeper_AuthData.authority).
|
||||
#token_keeper_AuthData{
|
||||
id = undefined,
|
||||
token = Token,
|
||||
status = active,
|
||||
context = Context,
|
||||
metadata = ?PARTY_METADATA(?TK_META_NS_APIKEYMGMT, SubjectID),
|
||||
authority = ?TK_AUTHORITY_KEYCLOAK
|
||||
} = call_get_by_token(Token, ?TOKEN_SOURCE_CONTEXT(), Client),
|
||||
_ = assert_context({api_key_token, JTI, SubjectID}, Context).
|
||||
|
||||
-spec detect_user_session_token_test(config()) -> ok.
|
||||
detect_user_session_token_test(C) ->
|
||||
@ -222,22 +261,15 @@ detect_user_session_token_test(C) ->
|
||||
SubjectID = <<"TEST">>,
|
||||
SubjectEmail = <<"test@test.test">>,
|
||||
{ok, Token} = issue_token(JTI, #{<<"sub">> => SubjectID, <<"email">> => SubjectEmail}, unlimited),
|
||||
AuthData = call_get_by_token(Token, ?TOKEN_SOURCE_CONTEXT(?USER_TOKEN_SOURCE), Client),
|
||||
_ = ct:pal("~p", [AuthData]),
|
||||
?assertEqual(undefined, AuthData#token_keeper_AuthData.id),
|
||||
?assertEqual(Token, AuthData#token_keeper_AuthData.token),
|
||||
?assertEqual(active, AuthData#token_keeper_AuthData.status),
|
||||
?assert(
|
||||
assert_context(
|
||||
{user_session_token, JTI, SubjectID, SubjectEmail, unlimited},
|
||||
AuthData#token_keeper_AuthData.context
|
||||
)
|
||||
),
|
||||
?assertMatch(
|
||||
?USER_METADATA(?TK_META_NS_KEYCLOAK, SubjectID, SubjectEmail),
|
||||
AuthData#token_keeper_AuthData.metadata
|
||||
),
|
||||
?assertEqual(?TK_AUTHORITY_KEYCLOAK, AuthData#token_keeper_AuthData.authority).
|
||||
#token_keeper_AuthData{
|
||||
id = undefined,
|
||||
token = Token,
|
||||
status = active,
|
||||
context = Context,
|
||||
metadata = ?USER_METADATA(?TK_META_NS_KEYCLOAK, SubjectID, SubjectEmail),
|
||||
authority = ?TK_AUTHORITY_KEYCLOAK
|
||||
} = call_get_by_token(Token, ?TOKEN_SOURCE_CONTEXT(?USER_TOKEN_SOURCE), Client),
|
||||
_ = assert_context({user_session_token, JTI, SubjectID, SubjectEmail, unlimited}, Context).
|
||||
|
||||
-spec detect_dummy_token_test(config()) -> ok.
|
||||
detect_dummy_token_test(C) ->
|
||||
@ -261,14 +293,94 @@ bouncer_context_from_claims_test(C) ->
|
||||
JTI = unique_id(),
|
||||
SubjectID = <<"TEST">>,
|
||||
{ok, Token} = issue_token_with_context(JTI, SubjectID),
|
||||
AuthData = call_get_by_token(Token, ?TOKEN_SOURCE_CONTEXT(), Client),
|
||||
_ = ct:pal("~p", [AuthData]),
|
||||
?assertEqual(undefined, AuthData#token_keeper_AuthData.id),
|
||||
?assertEqual(Token, AuthData#token_keeper_AuthData.token),
|
||||
?assertEqual(active, AuthData#token_keeper_AuthData.status),
|
||||
?assert(assert_context({claim_token, JTI}, AuthData#token_keeper_AuthData.context)),
|
||||
?assertMatch(?PARTY_METADATA(?TK_META_NS_APIKEYMGMT, SubjectID), AuthData#token_keeper_AuthData.metadata),
|
||||
?assertEqual(?TK_AUTHORITY_CAPI, AuthData#token_keeper_AuthData.authority).
|
||||
#token_keeper_AuthData{
|
||||
id = undefined,
|
||||
token = Token,
|
||||
status = active,
|
||||
context = Context,
|
||||
metadata = ?PARTY_METADATA(?TK_META_NS_APIKEYMGMT, SubjectID),
|
||||
authority = ?TK_AUTHORITY_CAPI
|
||||
} = call_get_by_token(Token, ?TOKEN_SOURCE_CONTEXT(), Client),
|
||||
_ = assert_context({claim_token, JTI}, Context).
|
||||
|
||||
-spec cons_claim_passthrough_test(config()) -> ok.
|
||||
cons_claim_passthrough_test(C) ->
|
||||
Client = mk_client(C),
|
||||
JTI = unique_id(),
|
||||
SubjectID = <<"TEST">>,
|
||||
{ok, Token} = issue_token_with_context(JTI, SubjectID, #{<<"cons">> => <<"client">>}),
|
||||
#token_keeper_AuthData{
|
||||
id = undefined,
|
||||
token = Token,
|
||||
status = active,
|
||||
context = Context,
|
||||
metadata = ?METADATA(?TK_META_NS_APIKEYMGMT, #{<<"party_id">> := SubjectID, <<"cons">> := <<"client">>}),
|
||||
authority = ?TK_AUTHORITY_CAPI
|
||||
} = call_get_by_token(Token, ?TOKEN_SOURCE_CONTEXT(), Client),
|
||||
_ = assert_context({claim_token, JTI}, Context).
|
||||
|
||||
-spec invoice_template_access_token_ok_test(config()) -> ok.
|
||||
invoice_template_access_token_ok_test(C) ->
|
||||
Client = mk_client(C),
|
||||
JTI = unique_id(),
|
||||
InvoiceTemplateID = unique_id(),
|
||||
SubjectID = <<"TEST">>,
|
||||
{ok, Token} = issue_token(
|
||||
JTI,
|
||||
#{
|
||||
<<"sub">> => SubjectID,
|
||||
<<"resource_access">> => #{
|
||||
?TK_RESOURCE_DOMAIN => #{
|
||||
<<"roles">> => [
|
||||
<<"party.*.invoice_templates.", InvoiceTemplateID/binary, ".invoice_template_invoices:write">>,
|
||||
<<"party.*.invoice_templates.", InvoiceTemplateID/binary, ":read">>
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
unlimited
|
||||
),
|
||||
#token_keeper_AuthData{
|
||||
id = undefined,
|
||||
token = Token,
|
||||
status = active,
|
||||
context = Context,
|
||||
metadata = ?PARTY_METADATA(?TK_META_NS_APIKEYMGMT, SubjectID),
|
||||
authority = ?TK_AUTHORITY_CAPI
|
||||
} = call_get_by_token(Token, ?TOKEN_SOURCE_CONTEXT(), Client),
|
||||
_ = assert_context({invoice_template_access_token, JTI, SubjectID, InvoiceTemplateID}, Context).
|
||||
|
||||
-spec invoice_template_access_token_no_access_test(config()) -> ok.
|
||||
invoice_template_access_token_no_access_test(C) ->
|
||||
Client = mk_client(C),
|
||||
JTI = unique_id(),
|
||||
SubjectID = <<"TEST">>,
|
||||
{ok, Token} = issue_token(JTI, #{<<"sub">> => SubjectID, <<"resource_access">> => #{}}, unlimited),
|
||||
#token_keeper_AuthDataNotFound{} =
|
||||
(catch call_get_by_token(Token, ?TOKEN_SOURCE_CONTEXT(), Client)).
|
||||
|
||||
-spec invoice_template_access_token_invalid_access_test(config()) -> ok.
|
||||
invoice_template_access_token_invalid_access_test(C) ->
|
||||
Client = mk_client(C),
|
||||
JTI = unique_id(),
|
||||
InvoiceID = unique_id(),
|
||||
SubjectID = <<"TEST">>,
|
||||
{ok, Token} = issue_token(
|
||||
JTI,
|
||||
#{
|
||||
<<"sub">> => SubjectID,
|
||||
<<"resource_access">> => #{
|
||||
?TK_RESOURCE_DOMAIN => #{
|
||||
<<"roles">> => [
|
||||
<<"invoices.", InvoiceID/binary, ":read">>
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
unlimited
|
||||
),
|
||||
#token_keeper_AuthDataNotFound{} =
|
||||
(catch call_get_by_token(Token, ?TOKEN_SOURCE_CONTEXT(), Client)).
|
||||
|
||||
%%
|
||||
|
||||
@ -303,34 +415,43 @@ get_service_spec(token_keeper) ->
|
||||
|
||||
assert_context(TokenInfo, EncodedContextFragment) ->
|
||||
#bctx_v1_ContextFragment{auth = Auth, user = User} = decode_bouncer_fragment(EncodedContextFragment),
|
||||
?assert(assert_auth(TokenInfo, Auth)),
|
||||
?assert(assert_user(TokenInfo, User)),
|
||||
true.
|
||||
_ = assert_auth(TokenInfo, Auth),
|
||||
_ = assert_user(TokenInfo, User).
|
||||
|
||||
assert_auth({claim_token, JTI}, Auth) ->
|
||||
?assertEqual(<<"ClaimToken">>, Auth#bctx_v1_Auth.method),
|
||||
?assertMatch(#bctx_v1_Token{id = JTI}, Auth#bctx_v1_Auth.token),
|
||||
true;
|
||||
?assertMatch(#bctx_v1_Token{id = JTI}, Auth#bctx_v1_Auth.token);
|
||||
assert_auth({api_key_token, JTI, SubjectID}, Auth) ->
|
||||
?assertEqual(<<"ApiKeyToken">>, Auth#bctx_v1_Auth.method),
|
||||
?assertMatch(#bctx_v1_Token{id = JTI}, Auth#bctx_v1_Auth.token),
|
||||
?assertMatch([#bctx_v1_AuthScope{party = ?CTX_ENTITY(SubjectID)}], Auth#bctx_v1_Auth.scope),
|
||||
true;
|
||||
?assertMatch([#bctx_v1_AuthScope{party = ?CTX_ENTITY(SubjectID)}], Auth#bctx_v1_Auth.scope);
|
||||
assert_auth({invoice_template_access_token, JTI, SubjectID, InvoiceTemplateID}, Auth) ->
|
||||
?assertEqual(<<"InvoiceTemplateAccessToken">>, Auth#bctx_v1_Auth.method),
|
||||
?assertMatch(#bctx_v1_Token{id = JTI}, Auth#bctx_v1_Auth.token),
|
||||
?assertMatch(
|
||||
[
|
||||
#bctx_v1_AuthScope{
|
||||
party = ?CTX_ENTITY(SubjectID),
|
||||
invoice_template = ?CTX_ENTITY(InvoiceTemplateID)
|
||||
}
|
||||
],
|
||||
Auth#bctx_v1_Auth.scope
|
||||
);
|
||||
assert_auth({user_session_token, JTI, _SubjectID, _SubjectEmail, Exp}, Auth) ->
|
||||
?assertEqual(<<"SessionToken">>, Auth#bctx_v1_Auth.method),
|
||||
?assertMatch(#bctx_v1_Token{id = JTI}, Auth#bctx_v1_Auth.token),
|
||||
?assertEqual(make_auth_expiration(Exp), Auth#bctx_v1_Auth.expiration),
|
||||
true.
|
||||
?assertEqual(make_auth_expiration(Exp), Auth#bctx_v1_Auth.expiration).
|
||||
|
||||
assert_user({claim_token, _}, undefined) ->
|
||||
true;
|
||||
ok;
|
||||
assert_user({api_key_token, _, _}, undefined) ->
|
||||
true;
|
||||
ok;
|
||||
assert_user({invoice_template_access_token, _, _, _}, undefined) ->
|
||||
ok;
|
||||
assert_user({user_session_token, _JTI, SubjectID, SubjectEmail, _Exp}, User) ->
|
||||
?assertEqual(SubjectID, User#bctx_v1_User.id),
|
||||
?assertEqual(SubjectEmail, User#bctx_v1_User.email),
|
||||
?assertEqual(?CTX_ENTITY(<<"external">>), User#bctx_v1_User.realm),
|
||||
true.
|
||||
?assertEqual(?CTX_ENTITY(<<"external">>), User#bctx_v1_User.realm).
|
||||
|
||||
%%
|
||||
|
||||
@ -346,6 +467,9 @@ issue_token(JTI, Claims0, Expiration) ->
|
||||
tk_token_jwt:issue(JTI, Claims, test).
|
||||
|
||||
issue_token_with_context(JTI, SubjectID) ->
|
||||
issue_token_with_context(JTI, SubjectID, #{}).
|
||||
|
||||
issue_token_with_context(JTI, SubjectID, AdditionalClaims) ->
|
||||
Acc0 = bouncer_context_helpers:empty(),
|
||||
Acc1 = bouncer_context_helpers:add_auth(
|
||||
#{
|
||||
@ -357,7 +481,7 @@ issue_token_with_context(JTI, SubjectID) ->
|
||||
FragmentContent = encode_context_fragment_content(Acc1),
|
||||
issue_token(
|
||||
JTI,
|
||||
#{
|
||||
AdditionalClaims#{
|
||||
<<"sub">> => SubjectID,
|
||||
<<"bouncer_ctx">> => #{
|
||||
<<"ty">> => <<"v1_thrift_binary">>,
|
||||
|
Loading…
Reference in New Issue
Block a user