ED-192: Add invoice_template_access_token extractor (#10)

This commit is contained in:
Alexey 2021-07-05 14:39:53 +03:00 committed by GitHub
parent aaf2428ab3
commit 81fb238b29
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 571 additions and 105 deletions

View File

@ -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 => #{

View File

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

View File

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

View File

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

View File

@ -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, #{}}.

View File

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

View File

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

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

View File

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

View File

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