From 5ae39c7d4dca14ca0cc00db0ea41f4826973774e Mon Sep 17 00:00:00 2001 From: Alexey S Date: Mon, 3 Oct 2022 15:44:41 +0400 Subject: [PATCH] TD-400: Extract resource access (#26) --- .env | 4 +- .github/workflows/erlang-checks.yml | 1 + rebar.lock | 16 +- src/tk_authdata.erl | 2 +- src/tk_authdata_source_context_extractor.erl | 6 +- src/tk_claim_utils.erl | 8 +- ...k_context_extractor_user_session_token.erl | 122 +++++++++++----- src/tk_handler_authenticator.erl | 2 +- src/tk_handler_authority_ephemeral.erl | 2 +- src/tk_handler_authority_offline.erl | 2 +- src/tk_storage_machinegun.erl | 13 +- src/tk_storage_machinegun_schema.erl | 7 +- test/token_keeper_SUITE.erl | 138 ++++++++++++++---- 13 files changed, 226 insertions(+), 97 deletions(-) diff --git a/.env b/.env index 48ead78..a42ad52 100644 --- a/.env +++ b/.env @@ -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 diff --git a/.github/workflows/erlang-checks.yml b/.github/workflows/erlang-checks.yml index 7eb63d6..a119883 100644 --- a/.github/workflows/erlang-checks.yml +++ b/.github/workflows/erlang-checks.yml @@ -37,3 +37,4 @@ jobs: use-thrift: true thrift-version: ${{ needs.setup.outputs.thrift-version }} run-ct-with-compose: true + cache-version: v2 diff --git a/rebar.lock b/rebar.lock index d73fe7f..7848d94 100644 --- a/rebar.lock +++ b/rebar.lock @@ -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">>, diff --git a/src/tk_authdata.erl b/src/tk_authdata.erl index 14d893b..1e49de8 100644 --- a/src/tk_authdata.erl +++ b/src/tk_authdata.erl @@ -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()}. diff --git a/src/tk_authdata_source_context_extractor.erl b/src/tk_authdata_source_context_extractor.erl index f6153c0..f5e029c 100644 --- a/src/tk_authdata_source_context_extractor.erl +++ b/src/tk_authdata_source_context_extractor.erl @@ -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} -> diff --git a/src/tk_claim_utils.erl b/src/tk_claim_utils.erl index 867a382..98e76b4 100644 --- a/src/tk_claim_utils.erl +++ b/src/tk_claim_utils.erl @@ -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 } diff --git a/src/tk_context_extractor_user_session_token.erl b/src/tk_context_extractor_user_session_token.erl index 3fb8334..f9626d3 100644 --- a/src/tk_context_extractor_user_session_token.erl +++ b/src/tk_context_extractor_user_session_token.erl @@ -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(#{ - ?CLAIM_USER_ID := UserID, - ?CLAIM_USER_EMAIL := UserEmail, - ?CLAIM_EXPIRES_AT := Expiration +extract_user_params(#{ + payload := #{ + ?CLAIM_USER_ID := UserID, + ?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)}}. + #{ + id => UserID, + email => UserEmail + }; +extract_user_params(TokenData) -> + RequiredKeys = [ + ?CLAIM_USER_ID, + ?CLAIM_USER_EMAIL + ], + throw({missing, RequiredKeys -- maps:keys(TokenData)}). -create_context(TokenID, TokenExpiration, UserID, UserEmail, UserRealm) -> +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 = bouncer_context_helpers:add_user( + Acc1 = append_user_context(UserParams, Acc0), + append_auth_context(AuthParams, Acc1). + +append_user_context(UserParams, BouncerCtx) -> + bouncer_context_helpers:add_user( #{ - id => UserID, - email => UserEmail, - realm => #{id => UserRealm} + 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) -> diff --git a/src/tk_handler_authenticator.erl b/src/tk_handler_authenticator.erl index 2e1ce15..7601d56 100644 --- a/src/tk_handler_authenticator.erl +++ b/src/tk_handler_authenticator.erl @@ -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]). diff --git a/src/tk_handler_authority_ephemeral.erl b/src/tk_handler_authority_ephemeral.erl index 2120da7..68a9733 100644 --- a/src/tk_handler_authority_ephemeral.erl +++ b/src/tk_handler_authority_ephemeral.erl @@ -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]). diff --git a/src/tk_handler_authority_offline.erl b/src/tk_handler_authority_offline.erl index bc9e0f5..6997f5e 100644 --- a/src/tk_handler_authority_offline.erl +++ b/src/tk_handler_authority_offline.erl @@ -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]). diff --git a/src/tk_storage_machinegun.erl b/src/tk_storage_machinegun.erl index 2ac152b..aaba2c6 100644 --- a/src/tk_storage_machinegun.erl +++ b/src/tk_storage_machinegun.erl @@ -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}). diff --git a/src/tk_storage_machinegun_schema.erl b/src/tk_storage_machinegun_schema.erl index a276c16..20e23bd 100644 --- a/src/tk_storage_machinegun_schema.erl +++ b/src/tk_storage_machinegun_schema.erl @@ -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), diff --git a/test/token_keeper_SUITE.erl b/test/token_keeper_SUITE.erl index f92fe84..a9dea0f 100644 --- a/test/token_keeper_SUITE.erl +++ b/test/token_keeper_SUITE.erl @@ -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). %%