TD-400: Extract resource access (#26)

This commit is contained in:
Alexey S 2022-10-03 15:44:41 +04:00 committed by GitHub
parent 8cc0d3036c
commit 5ae39c7d4d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 226 additions and 97 deletions

4
.env
View File

@ -2,6 +2,6 @@
# You SHOULD specify point releases here so that build time and run time Erlang/OTPs
# are the same. See: https://github.com/erlware/relx/pull/902
SERVICE_NAME=token-keeper
OTP_VERSION=24.2.0
OTP_VERSION=24
REBAR_VERSION=3.18
THRIFT_VERSION=0.14.2.2
THRIFT_VERSION=0.14.2.3

View File

@ -37,3 +37,4 @@ jobs:
use-thrift: true
thrift-version: ${{ needs.setup.outputs.thrift-version }}
run-ct-with-compose: true
cache-version: v2

View File

@ -1,11 +1,11 @@
{"1.2.0",
[{<<"bouncer_client">>,
{git,"https://github.com/valitydev/bouncer-client-erlang.git",
{ref,"b6c7be05e24f46121f42ae5a48232b94f78c9c5c"}},
{ref,"79d9d0144ed66537ec25302aeba8f133bddb05d7"}},
0},
{<<"bouncer_proto">>,
{git,"https://github.com/valitydev/bouncer-proto",
{ref,"633ba73e376ab06587499bd163cf807a9c34b8f7"}},
{ref,"6ce6d33af5346c84b99fc6a3cebb5070001d0b62"}},
1},
{<<"cache">>,{pkg,<<"cache">>,<<"2.3.3">>},1},
{<<"certifi">>,{pkg,<<"certifi">>,<<"2.8.0">>},2},
@ -30,23 +30,23 @@
{<<"jsx">>,{pkg,<<"jsx">>,<<"3.1.0">>},0},
{<<"machinery">>,
{git,"https://github.com/valitydev/machinery-erlang.git",
{ref,"62c32434c80a462956ad9d50f9bce47836580d77"}},
{ref,"19cdc6c846f0ebf65ae193ca0988e353e53fe1d5"}},
0},
{<<"metrics">>,{pkg,<<"metrics">>,<<"1.0.1">>},2},
{<<"mg_proto">>,
{git,"https://github.com/valitydev/machinegun-proto.git",
{ref,"7d780d5aa445e37b4816ac8a433bfaffe3715f63"}},
{git,"https://github.com/valitydev/machinegun-proto",
{ref,"96f7f11b184c29d8b7e83cd7646f3f2c13662bda"}},
1},
{<<"mimerl">>,{pkg,<<"mimerl">>,<<"1.2.0">>},2},
{<<"org_management_proto">>,
{git,"https://github.com/valitydev/org-management-proto",
{ref,"f433223706284000694e54e839fafb10db84e2b3"}},
{ref,"03a269df4805fa604e8fd2d04241619a739e2ae3"}},
1},
{<<"parse_trans">>,{pkg,<<"parse_trans">>,<<"3.3.1">>},2},
{<<"ranch">>,{pkg,<<"ranch">>,<<"1.8.0">>},2},
{<<"scoper">>,
{git,"https://github.com/valitydev/scoper.git",
{ref,"7f3183df279bc8181efe58dafd9cae164f495e6f"}},
{ref,"87110f5bd72c0e39ba9b7d6eca88fea91b8cd357"}},
0},
{<<"snowflake">>,
{git,"https://github.com/valitydev/snowflake.git",
@ -59,7 +59,7 @@
0},
{<<"token_keeper_proto">>,
{git,"https://github.com/valitydev/token-keeper-proto.git",
{ref,"8f7016f68692fc8e3141ba0fce2d47b6c8b6102a"}},
{ref,"ea6e935122aa33467e53370a1509d7341be98750"}},
0},
{<<"unicode_util_compat">>,{pkg,<<"unicode_util_compat">>,<<"0.7.0">>},2},
{<<"woody">>,

View File

@ -14,7 +14,7 @@
-type id() :: binary().
-type status() :: active | revoked.
-type encoded_context_fragment() :: tk_context_thrift:'ContextFragment'().
-type encoded_context_fragment() :: tk_token_keeper_thrift:'ContextFragment'().
-type authority_id() :: binary().
-type metadata() :: #{binary() => binary()}.

View File

@ -1,7 +1,7 @@
-module(tk_authdata_source_context_extractor).
-behaviour(tk_authdata_source).
-include_lib("token_keeper_proto/include/tk_context_thrift.hrl").
-include_lib("bouncer_proto/include/bouncer_ctx_thrift.hrl").
%% Behaviour
@ -54,13 +54,13 @@ make_auth_data(ContextFragment, Metadata) ->
}).
encode_context_fragment(ContextFragment) ->
#bctx_ContextFragment{
#ctx_ContextFragment{
type = v1_thrift_binary,
content = encode_context_fragment_content(ContextFragment)
}.
encode_context_fragment_content(ContextFragment) ->
Type = {struct, struct, {bouncer_context_v1_thrift, 'ContextFragment'}},
Type = {struct, struct, {bouncer_ctx_v1_thrift, 'ContextFragment'}},
Codec = thrift_strict_binary_codec:new(),
case thrift_strict_binary_codec:write(Codec, Type, ContextFragment) of
{ok, Codec1} ->

View File

@ -1,6 +1,6 @@
-module(tk_claim_utils).
-include_lib("token_keeper_proto/include/tk_context_thrift.hrl").
-include_lib("bouncer_proto/include/bouncer_ctx_thrift.hrl").
-export([decode_authdata/1]).
-export([encode_authdata/1]).
@ -11,7 +11,7 @@
%%
-type authdata() :: tk_authdata:prototype().
-type encoded_context_fragment() :: tk_context_thrift:'ContextFragment'().
-type encoded_context_fragment() :: tk_token_keeper_thrift:'ContextFragment'().
-type claim() :: term().
-type claims() :: tk_token:payload().
@ -58,7 +58,7 @@ decode_bouncer_claim(#{
?CLAIM_CTX_CONTEXT := Content
}) ->
try
{ok, #bctx_ContextFragment{
{ok, #ctx_ContextFragment{
type = v1_thrift_binary,
content = base64:decode(Content)
}}
@ -73,7 +73,7 @@ decode_bouncer_claim(Ctx) ->
-spec encode_bouncer_claim(encoded_context_fragment()) -> claims().
encode_bouncer_claim(
#bctx_ContextFragment{
#ctx_ContextFragment{
type = v1_thrift_binary,
content = Content
}

View File

@ -1,4 +1,5 @@
-module(tk_context_extractor_user_session_token).
-behaviour(tk_context_extractor).
-export([extract_context/2]).
@ -21,58 +22,88 @@
-define(CLAIM_USER_ID, <<"sub">>).
-define(CLAIM_USER_EMAIL, <<"email">>).
-define(CLAIM_EXPIRES_AT, <<"exp">>).
-define(CLAIM_RESOURCE_ACCESS, <<"resource_access">>).
%% API functions
-spec extract_context(tk_token:token_data(), opts()) -> tk_context_extractor:extracted_context() | undefined.
extract_context(#{id := TokenID, payload := Payload}, Opts) ->
case extract_payload_data(Payload) of
{ok, {UserID, UserEmail, Expiration}} ->
create_context_and_metadata(TokenID, Expiration, UserID, UserEmail, Opts);
{error, Reason} ->
extract_context(TokenData, Opts) ->
try
AuthParams = extract_auth_params(TokenData),
UserParams = add_user_realm(extract_user_params(TokenData), Opts),
Context = create_context(UserParams, AuthParams),
Metadata = create_metadata(UserParams),
{Context, wrap_metadata(Metadata, Opts)}
catch
throw:Reason ->
_ = logger:warning("Could not extract user_session_token context, reason: ~p", [Reason]),
undefined
end.
%% Internal functions
create_context_and_metadata(TokenID, TokenExpiration, UserID, UserEmail, Opts) ->
UserRealm = maps:get(user_realm, Opts),
{
create_context(TokenID, TokenExpiration, UserID, UserEmail, UserRealm),
wrap_metadata(
create_metadata(UserID, UserEmail, UserRealm),
Opts
)
}.
extract_payload_data(#{
extract_user_params(#{
payload := #{
?CLAIM_USER_ID := UserID,
?CLAIM_USER_EMAIL := UserEmail,
?CLAIM_EXPIRES_AT := Expiration
?CLAIM_USER_EMAIL := UserEmail
}
}) ->
{ok, {UserID, UserEmail, Expiration}};
extract_payload_data(Payload) ->
RequiredKeys = [?CLAIM_USER_ID, ?CLAIM_USER_EMAIL, ?CLAIM_EXPIRES_AT],
{error, {missing, RequiredKeys -- maps:keys(Payload)}}.
create_context(TokenID, TokenExpiration, UserID, UserEmail, UserRealm) ->
Acc0 = bouncer_context_helpers:empty(),
Acc1 = bouncer_context_helpers:add_user(
#{
id => UserID,
email => UserEmail,
realm => #{id => UserRealm}
email => UserEmail
};
extract_user_params(TokenData) ->
RequiredKeys = [
?CLAIM_USER_ID,
?CLAIM_USER_EMAIL
],
throw({missing, RequiredKeys -- maps:keys(TokenData)}).
extract_auth_params(#{
id := TokenID,
payload := #{
?CLAIM_EXPIRES_AT := TokenExp
} = Payload
}) ->
genlib_map:compact(#{
token_id => TokenID,
token_exp => TokenExp,
resource_access => genlib_map:get(?CLAIM_RESOURCE_ACCESS, Payload)
});
extract_auth_params(TokenData) ->
RequiredKeys = [
?CLAIM_EXPIRES_AT
],
throw({missing, RequiredKeys -- maps:keys(TokenData)}).
add_user_realm(UserParams, Opts) ->
UserParams#{realm => maps:get(user_realm, Opts)}.
create_context(UserParams, AuthParams) ->
Acc0 = bouncer_context_helpers:empty(),
Acc1 = append_user_context(UserParams, Acc0),
append_auth_context(AuthParams, Acc1).
append_user_context(UserParams, BouncerCtx) ->
bouncer_context_helpers:add_user(
#{
id => maps:get(id, UserParams),
email => maps:get(email, UserParams),
realm => #{id => maps:get(realm, UserParams)}
},
Acc0
),
BouncerCtx
).
append_auth_context(AuthParams, BouncerCtx) ->
bouncer_context_helpers:add_auth(
#{
method => <<"SessionToken">>,
expiration => make_auth_expiration(TokenExpiration),
token => #{id => TokenID}
expiration => make_auth_expiration(maps:get(token_exp, AuthParams)),
token => genlib_map:compact(#{
id => maps:get(token_id, AuthParams),
access => maybe_auth_access_list(AuthParams)
})
},
Acc1
BouncerCtx
).
make_auth_expiration(0) ->
@ -80,11 +111,26 @@ make_auth_expiration(0) ->
make_auth_expiration(Timestamp) when is_integer(Timestamp) ->
genlib_rfc3339:format(Timestamp, second).
create_metadata(UserID, UserEmail, UserRealm) ->
maybe_auth_access_list(#{resource_access := ResourceAccess}) ->
maps:fold(
fun(Key, Value, Acc) ->
Entry = #{
id => Key,
roles => maps:get(<<"roles">>, Value)
},
[Entry | Acc]
end,
[],
ResourceAccess
);
maybe_auth_access_list(_) ->
undefined.
create_metadata(UserParams) ->
#{
user_id => UserID,
user_email => UserEmail,
user_realm => UserRealm
user_id => maps:get(id, UserParams),
user_email => maps:get(email, UserParams),
user_realm => maps:get(realm, UserParams)
}.
wrap_metadata(Metadata, ExtractorOpts) ->

View File

@ -1,6 +1,6 @@
-module(tk_handler_authenticator).
-include_lib("token_keeper_proto/include/tk_context_thrift.hrl").
-include_lib("bouncer_proto/include/bouncer_ctx_thrift.hrl").
-include_lib("token_keeper_proto/include/tk_token_keeper_thrift.hrl").
-export([get_handler_spec/1]).

View File

@ -1,6 +1,6 @@
-module(tk_handler_authority_ephemeral).
-include_lib("token_keeper_proto/include/tk_context_thrift.hrl").
-include_lib("bouncer_proto/include/bouncer_ctx_thrift.hrl").
-include_lib("token_keeper_proto/include/tk_token_keeper_thrift.hrl").
-export([get_handler_spec/2]).

View File

@ -1,6 +1,6 @@
-module(tk_handler_authority_offline).
-include_lib("token_keeper_proto/include/tk_context_thrift.hrl").
-include_lib("bouncer_proto/include/bouncer_ctx_thrift.hrl").
-include_lib("token_keeper_proto/include/tk_token_keeper_thrift.hrl").
-export([get_handler_spec/2]).

View File

@ -15,6 +15,7 @@
-export([process_repair/4]).
-export([process_timeout/3]).
-export([process_call/4]).
-export([process_notification/4]).
-type storage_opts() :: #{
namespace := namespace(),
@ -108,12 +109,16 @@ process_call(revoke, #{history := History}, _, _) ->
Events = change_status(revoked, AuthData),
{ok, #{events => Events}}.
-spec process_notification(machinery:args(_), machine(), handler_args(), handler_opts()) -> no_return().
process_notification(_Args, _Machine, _, _) ->
erlang:error({not_implemented, process_notification}).
%%-------------------------------------
%% internal
create_authdata(AuthData) ->
[
{created, #tk_events_AuthDataCreated{
{created, #events_AuthDataCreated{
id = maps:get(id, AuthData),
status = maps:get(status, AuthData),
context = maps:get(context, AuthData),
@ -124,7 +129,7 @@ create_authdata(AuthData) ->
change_status(NewStatus, #{status := NewStatus}) ->
[];
change_status(NewStatus, #{status := _OtherStatus}) ->
[{status_changed, #tk_events_AuthDataStatusChanged{status = NewStatus}}].
[{status_changed, #events_AuthDataStatusChanged{status = NewStatus}}].
%%
@ -158,8 +163,8 @@ collapse_history(History) ->
collapse_history([], AuthData) when AuthData =/= undefined ->
AuthData;
collapse_history([{_, _, {created, AuthData}} | Rest], undefined) ->
#tk_events_AuthDataCreated{id = ID, context = Ctx, status = Status, metadata = Meta} = AuthData,
#events_AuthDataCreated{id = ID, context = Ctx, status = Status, metadata = Meta} = AuthData,
collapse_history(Rest, #{id => ID, context => Ctx, status => Status, metadata => Meta});
collapse_history([{_, _, {status_changed, StatusChanged}} | Rest], AuthData) when AuthData =/= undefined ->
#tk_events_AuthDataStatusChanged{status = Status} = StatusChanged,
#events_AuthDataStatusChanged{status = Status} = StatusChanged,
collapse_history(Rest, AuthData#{status => Status}).

View File

@ -113,6 +113,7 @@ deserialize(Type, Data) ->
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
-include_lib("token_keeper_proto/include/tk_events_thrift.hrl").
-include_lib("bouncer_proto/include/bouncer_ctx_thrift.hrl").
-spec test() -> _.
@ -126,10 +127,10 @@ deserialize(Type, Data) ->
marshal_unmarshal_created_test() ->
Event =
{created, #tk_events_AuthDataCreated{
{created, #events_AuthDataCreated{
id = <<"TEST">>,
status = active,
context = #bctx_ContextFragment{type = v1_thrift_binary, content = <<"STUFF">>},
context = #ctx_ContextFragment{type = v1_thrift_binary, content = <<"STUFF">>},
metadata = #{}
}},
{Marshaled, _} = marshal_event(1, Event, ?CONTEXT),
@ -138,7 +139,7 @@ marshal_unmarshal_created_test() ->
marshal_unmarshal_status_changed_test() ->
Event =
{status_changed, #tk_events_AuthDataStatusChanged{
{status_changed, #events_AuthDataStatusChanged{
status = revoked
}},
{Marshaled, _} = marshal_event(1, Event, ?CONTEXT),

View File

@ -4,10 +4,10 @@
-include_lib("stdlib/include/assert.hrl").
-include_lib("token_keeper_proto/include/tk_token_keeper_thrift.hrl").
-include_lib("token_keeper_proto/include/tk_context_thrift.hrl").
-include_lib("bouncer_proto/include/bouncer_ctx_thrift.hrl").
-include_lib("bouncer_proto/include/bouncer_base_thrift.hrl").
-include_lib("bouncer_proto/include/bouncer_context_v1_thrift.hrl").
-include_lib("bouncer_proto/include/bouncer_ctx_v1_thrift.hrl").
-export([all/0]).
-export([groups/0]).
@ -25,6 +25,8 @@
-export([authenticate_phony_api_key_token_ok/1]).
-export([authenticate_user_session_token_ok/1]).
-export([authenticate_user_session_token_w_exp_ok/1]).
-export([authenticate_user_session_token_no_exp_fail/1]).
-export([authenticate_user_session_token_w_resource_access/1]).
-export([authenticate_blacklisted_jti_fail/1]).
-export([authenticate_non_blacklisted_jti_ok/1]).
-export([authenticate_ephemeral_claim_token_ok/1]).
@ -89,7 +91,9 @@ groups() ->
authenticate_user_session_token_no_payload_claims_fail,
authenticate_phony_api_key_token_ok,
authenticate_user_session_token_ok,
authenticate_user_session_token_w_exp_ok
authenticate_user_session_token_w_exp_ok,
authenticate_user_session_token_no_exp_fail,
authenticate_user_session_token_w_resource_access
]},
{ephemeral, [parallel], [
authenticate_invalid_token_type_fail,
@ -365,7 +369,7 @@ authenticate_phony_api_key_token_ok(C) ->
metadata = #{?META_PARTY_ID := SubjectID},
authority = ?TK_AUTHORITY_KEYCLOAK
} = call_authenticate(Token, ?TOKEN_SOURCE_CONTEXT, C),
_ = assert_context({api_key_token, JTI, SubjectID}, Context).
_ = assert_context({api_key_token, #{jti => JTI, subject_id => SubjectID}}, Context).
-spec authenticate_user_session_token_ok(config()) -> _.
authenticate_user_session_token_ok(C) ->
@ -386,7 +390,22 @@ authenticate_user_session_token_ok(C) ->
},
authority = ?TK_AUTHORITY_KEYCLOAK
} = call_authenticate(Token, ?TOKEN_SOURCE_CONTEXT(?USER_TOKEN_SOURCE), C),
_ = assert_context({user_session_token, JTI, SubjectID, SubjectEmail, undefined}, Context).
_ = assert_context(
{user_session_token, #{jti => JTI, subject_id => SubjectID, subject_email => SubjectEmail}},
Context
).
-spec authenticate_user_session_token_no_exp_fail(config()) -> _.
authenticate_user_session_token_no_exp_fail(C) ->
JTI = unique_id(),
SubjectID = unique_id(),
SubjectEmail = <<"test@test.test">>,
Claims = get_user_session_token_claims(JTI, 0, SubjectID, SubjectEmail),
Token = issue_token(maps:remove(<<"exp">>, Claims), C),
?assertThrow(
#token_keeper_AuthDataNotFound{},
call_authenticate(Token, ?TOKEN_SOURCE_CONTEXT(?USER_TOKEN_SOURCE), C)
).
-spec authenticate_user_session_token_w_exp_ok(config()) -> _.
authenticate_user_session_token_w_exp_ok(C) ->
@ -398,7 +417,40 @@ authenticate_user_session_token_w_exp_ok(C) ->
#token_keeper_AuthData{
context = Context
} = call_authenticate(Token, ?TOKEN_SOURCE_CONTEXT(?USER_TOKEN_SOURCE), C),
_ = assert_context({user_session_token, JTI, SubjectID, SubjectEmail, make_auth_expiration(42)}, Context).
_ = assert_context(
{user_session_token, #{
jti => JTI, subject_id => SubjectID, subject_email => SubjectEmail, exp => make_auth_expiration(42)
}},
Context
).
-spec authenticate_user_session_token_w_resource_access(config()) -> _.
authenticate_user_session_token_w_resource_access(C) ->
JTI = unique_id(),
SubjectID = unique_id(),
SubjectEmail = <<"test@test.test">>,
ResourceAccess = #{
<<"api.test">> => #{
<<"roles">> => [<<"do.nothing">>]
}
},
Claims = get_user_session_token_claims(JTI, 42, SubjectID, SubjectEmail, ResourceAccess),
Token = issue_token(Claims, C),
#token_keeper_AuthData{
context = Context
} = call_authenticate(Token, ?TOKEN_SOURCE_CONTEXT(?USER_TOKEN_SOURCE), C),
_ = assert_context(
{user_session_token, #{
jti => JTI,
subject_id => SubjectID,
subject_email => SubjectEmail,
exp => make_auth_expiration(42),
access => [
{<<"api.test">>, [<<"do.nothing">>]}
]
}},
Context
).
-spec authenticate_user_session_token_no_payload_claims_fail(config()) -> _.
authenticate_user_session_token_no_payload_claims_fail(C) ->
@ -447,7 +499,7 @@ authenticate_ephemeral_claim_token_ok(C) ->
metadata = Metadata,
authority = AuthorityID
} = call_authenticate(Token, ?TOKEN_SOURCE_CONTEXT, C),
_ = assert_context({claim_token, JTI}, Context).
_ = assert_context({claim_token, #{jti => JTI}}, Context).
-spec issue_ephemeral_token_ok(config()) -> _.
issue_ephemeral_token_ok(C) ->
@ -503,7 +555,7 @@ authenticate_offline_token_ok(C) ->
metadata = Metadata,
authority = AuthorityID
} = call_authenticate(Token, ?TOKEN_SOURCE_CONTEXT, C),
_ = assert_context({claim_token, JTI}, Context).
_ = assert_context({claim_token, #{jti => JTI}}, Context).
-spec issue_offline_token_ok(config()) -> _.
issue_offline_token_ok(C) ->
@ -600,7 +652,17 @@ get_phony_api_key_claims(JTI, SubjectID) ->
maps:merge(#{<<"sub">> => SubjectID}, get_base_claims(JTI)).
get_user_session_token_claims(JTI, Exp, SubjectID, SubjectEmail) ->
maps:merge(#{<<"sub">> => SubjectID, <<"email">> => SubjectEmail}, get_base_claims(JTI, Exp)).
get_user_session_token_claims(JTI, Exp, SubjectID, SubjectEmail, undefined).
get_user_session_token_claims(JTI, Exp, SubjectID, SubjectEmail, ResourceAccess) ->
maps:merge(
genlib_map:compact(#{
<<"sub">> => SubjectID,
<<"email">> => SubjectEmail,
<<"resource_access">> => ResourceAccess
}),
get_base_claims(JTI, Exp)
).
create_bouncer_context(JTI) ->
bouncer_context_helpers:add_auth(
@ -613,7 +675,7 @@ create_bouncer_context(JTI) ->
create_encoded_bouncer_context(JTI) ->
Fragment = create_bouncer_context(JTI),
#bctx_ContextFragment{
#ctx_ContextFragment{
type = v1_thrift_binary,
content = encode_context_fragment_content(Fragment)
}.
@ -671,47 +733,61 @@ get_service_spec({token_ephemeral_authority, _}) ->
%%
-define(CTX_ENTITY(ID), #bouncer_base_Entity{id = ID}).
-define(CTX_ENTITY(ID), #base_Entity{id = ID}).
encode_context_fragment_content(ContextFragment) ->
Type = {struct, struct, {bouncer_context_v1_thrift, 'ContextFragment'}},
Type = {struct, struct, {bouncer_ctx_v1_thrift, 'ContextFragment'}},
Codec = thrift_strict_binary_codec:new(),
case thrift_strict_binary_codec:write(Codec, Type, ContextFragment) of
{ok, Codec1} ->
thrift_strict_binary_codec:close(Codec1)
end.
decode_bouncer_fragment(#bctx_ContextFragment{type = v1_thrift_binary, content = Content}) ->
Type = {struct, struct, {bouncer_context_v1_thrift, 'ContextFragment'}},
decode_bouncer_fragment(#ctx_ContextFragment{type = v1_thrift_binary, content = Content}) ->
Type = {struct, struct, {bouncer_ctx_v1_thrift, 'ContextFragment'}},
Codec = thrift_strict_binary_codec:new(Content),
{ok, Fragment, _} = thrift_strict_binary_codec:read(Codec, Type),
Fragment.
assert_context(TokenInfo, EncodedContextFragment) ->
#bctx_v1_ContextFragment{auth = Auth, user = User} = decode_bouncer_fragment(EncodedContextFragment),
#ctx_v1_ContextFragment{auth = Auth, user = User} = decode_bouncer_fragment(EncodedContextFragment),
_ = 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);
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);
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(Exp, Auth#bctx_v1_Auth.expiration).
assert_auth({claim_token, #{jti := JTI}}, Auth) ->
?assertEqual(<<"ClaimToken">>, Auth#ctx_v1_Auth.method),
?assertMatch(#ctx_v1_Token{id = JTI}, Auth#ctx_v1_Auth.token);
assert_auth({api_key_token, #{jti := JTI, subject_id := SubjectID}}, Auth) ->
?assertEqual(<<"ApiKeyToken">>, Auth#ctx_v1_Auth.method),
?assertMatch(#ctx_v1_Token{id = JTI}, Auth#ctx_v1_Auth.token),
?assertMatch([#ctx_v1_AuthScope{party = ?CTX_ENTITY(SubjectID)}], Auth#ctx_v1_Auth.scope);
assert_auth({user_session_token, #{jti := JTI} = TokenInfo}, Auth) ->
?assertEqual(<<"SessionToken">>, Auth#ctx_v1_Auth.method),
Exp = maps:get(exp, TokenInfo, undefined),
Access =
case maps:get(access, TokenInfo, undefined) of
undefined ->
undefined;
AccessList ->
[
#ctx_v1_ResourceAccess{
id = ID,
roles = Roles
}
|| {ID, Roles} <- AccessList
]
end,
?assertMatch(#ctx_v1_Token{id = JTI, access = Access}, Auth#ctx_v1_Auth.token),
?assertEqual(Exp, Auth#ctx_v1_Auth.expiration).
assert_user({claim_token, _}, undefined) ->
ok;
assert_user({api_key_token, _, _}, undefined) ->
assert_user({api_key_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).
assert_user({user_session_token, #{subject_id := SubjectID, subject_email := SubjectEmail}}, User) ->
?assertEqual(SubjectID, User#ctx_v1_User.id),
?assertEqual(SubjectEmail, User#ctx_v1_User.email),
?assertEqual(?CTX_ENTITY(<<"external">>), User#ctx_v1_User.realm).
%%