ED-298: Implement new thrift interface (#17)

This commit is contained in:
Alexey S 2021-12-10 11:18:47 +03:00 committed by GitHub
parent 197b0f786f
commit 30d303f47d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
53 changed files with 2999 additions and 2551 deletions

2
Jenkinsfile vendored
View File

@ -18,6 +18,6 @@ build('token_keeper', 'docker-host', finalHook) {
pipeErlangService = load("${env.JENKINS_LIB}/pipeErlangService.groovy")
}
pipeErlangService.runPipe(true, true)
pipeErlangService.runPipe(true, false)
}

View File

@ -14,10 +14,10 @@ SERVICE_IMAGE_PUSH_TAG ?= $(SERVICE_IMAGE_TAG)
# Base image for the service
BASE_IMAGE_NAME := service-erlang
BASE_IMAGE_TAG := 0c1352dbf4a31afe0df372b59699a88f3af7986f
BASE_IMAGE_TAG := 5ea1e10733d806e40761b6c8eec93fc0c9657992
BUILD_IMAGE_NAME := build-erlang
BUILD_IMAGE_TAG := 61a001bbb48128895735a3ac35b0858484fdb2eb
BUILD_IMAGE_TAG := 785d48cbfa7e7f355300c08ba9edc6f0e78810cb
CALL_ANYWHERE := \
submodules \
all compile xref lint dialyze cover release clean distclean \

View File

@ -1,19 +1,7 @@
[
{token_keeper, [
{ip, "::"},
{port, 8022},
{services, #{
token_keeper => #{
path => <<"/v1/token-keeper">>
}
}},
{service_clients, #{
automaton => #{
url => <<"http://machinegun:8022/v1/automaton">>
}
}},
{protocol_opts, #{
% How much to wait for another request before closing a keepalive connection? (ms)
request_timeout => 5000
@ -26,7 +14,6 @@
}},
% How much to wait for outstanding requests completion when asked to shut down? (ms)
{shutdown_timeout, 1000},
{woody_event_handlers, [
hay_woody_event_handler,
{scoper_woody_event_handler, #{
@ -38,106 +25,163 @@
}
}}
]},
{health_check, #{
% disk => {erl_health, disk , ["/", 99]},
% memory => {erl_health, cg_memory, [99]},
% service => {erl_health, service , [<<"bouncer">>]}
}},
{jwt, #{
keyset => #{
test => #{
source => {pem_file, "keys/local/private.pem"},
%% Who is the authority for this token
authority => keycloak
}
}
}},
%% Token issuing configuration
{issuing, #{
authority => keycloak
}},
%% Storage configuration
{storage, {machinegun, #{}}},
%% Token blacklisting
{blacklist, #{
%% Path to a .yaml file containing token blacklist entries
%% Refer to `config/token-blacklist.example.yaml` for information about its format
%% Blacklisting functionality is disabled when no file path is provided
path => "token-blacklist.yaml"
}},
%% Authority configuration
{authorities, #{
keycloak => #{
%% What to use as authority id string
id => <<"test.rbkmoney.keycloak">>,
%% Which jwt key to use for signing for this authority
%% You can only use keys that have the same authority configured for them
signer => test,
%% Where to fetch authdata for tokens issued by this authority
authdata_sources => [
%% Fetch from claim
{claim, #{
%% Enable compatibility with legacy issued tokens when getting from storage
compatibility => {true, #{
%% Where to put metadata
metadata_mappings => #{
party_id => <<"test.rbkmoney.party.id">>,
consumer => <<"test.rbkmoney.capi.consumer">>
<<"com.rbkmoney.access.capi">> =>
#{
%% Woody service config
service => #{
path => <<"/v2/authority/com.rbkmoney.access.capi">>
},
type =>
{ephemeral, #{
%% Token issuing config
token => #{
type => jwt
}
}}
}},
%% Fetch from storage
{storage, #{}},
%% Create a new bouncer context using token data
{extract, #{
%% Configuration for how to extract said context
methods => [
%% Create bouncer context from various legacy data the token has to offer
%% 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 => #{
%% Where to put metadata
metadata_mappings => #{
party_id => <<"test.rbkmoney.party.id">>
}
},
%% user_session_token options to use (can be used standalone)
user_session_token_opts => #{
%% Realm of the user
user_realm => <<"external">>,
%% Where to put metadata
metadata_mappings => #{
user_id => <<"test.rbkmoney.user.id">>,
user_email => <<"test.rbkmoney.user.email">>,
user_realm => <<"test.rbkmoney.user.realm">>
}
},
%% List of origins using which we perform token classification
user_session_token_origins => [<<"http://localhost">>]
},
<<"com.rbkmoney.apikeymgmt">> =>
#{
%% Woody service config
service => #{
path => <<"/v2/authority/com.rbkmoney.apikeymgmt">>
},
%% Woody handler config
type =>
{offline, #{
%% Token issuing config
token => #{
type => jwt
},
%% Storage config
storage => #{
name => <<"com.rbkmoney.apikeymgmt">>
}
}}
}
}},
{authenticator, #{
service => #{
path => <<"/v2/authenticator">>
},
authorities => #{
<<"com.rbkmoney.keycloak">> =>
#{
sources => [
%% Fetch from claim
{legacy_claim, #{
%% Where to put metadata
metadata_mappings => #{
party_id => <<"test.rbkmoney.party.id">>,
consumer => <<"test.rbkmoney.capi.consumer">>
}
}},
%% Create a new bouncer context using token data
{extract, #{
%% Configuration for how to extract said context
methods => [
%% Create bouncer context from various legacy data the token has to offer
%% 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 => #{
%% Where to put metadata
metadata_mappings => #{
party_id => <<"test.rbkmoney.party.id">>
}
},
%% user_session_token options to use (can be used standalone)
user_session_token_opts => #{
%% Realm of the user
user_realm => <<"external">>,
%% Where to put metadata
metadata_mappings => #{
user_id => <<"test.rbkmoney.user.id">>,
user_email => <<"test.rbkmoney.user.email">>,
user_realm => <<"test.rbkmoney.user.realm">>
}
},
%% List of origins using which we perform token classification
user_session_token_origins => [<<"http://localhost">>]
}}
]
}}
]
}}
]
},
<<"com.rbkmoney.apikeymgmt">> =>
#{
sources => [
{storage, #{
name => <<"com.rbkmoney.apikeymgmt">>
}}
]
},
<<"com.rbkmoney.access.capi">> =>
#{
sources => [
{claim, #{}}
]
}
}
}},
{tokens, #{
jwt => #{
%% Provides the mapping between key id and authority id
authority_bindings => #{
<<"com.rbkmoney.apikeymgmt">> => <<"apikeymgmt">>,
<<"com.rbkmoney.access.capi">> => <<"access.capi">>,
<<"com.rbkmoney.keycloak">> => <<"keycloak">>
},
%% Provides a set of keys
keyset => #{
<<"apikeymgmt">> => #{
source => {pem_file, "keys/apikeymgmt/private.pem"}
},
<<"access.capi">> => #{
source => {pem_file, "keys/capi/private.pem"}
},
<<"keycloak">> => #{
source => {pem_file, "keys/keycloak/public.pem"}
}
}
}
}},
{storages, #{
<<"com.rbkmoney.apikeymgmt">> =>
{machinegun, #{
namespace => apikeymgmt,
automaton => #{
url => <<"http://machinegun:8022/v1/automaton">>,
event_handler => [scoper_woody_event_handler],
transport_opts => #{}
}
}}
}},
{machinegun, #{
processor => #{
path => <<"/v2/stateproc">>
}
}}
]},
{kernel, [
{logger_level, debug},
{logger, [
@ -149,7 +193,6 @@
}}
]}
]},
% {how_are_you, [
% {metrics_publishers, [
% {hay_statsd_publisher, #{
@ -159,14 +202,12 @@
% }}
% ]}
% ]},
{scoper, [
{storage, scoper_storage_logger}
]},
{snowflake, [
{max_backward_clock_moving, 1000} % 1 second
% 1 second
{max_backward_clock_moving, 1000}
% {machine_id, hostname_hash}
]}
].

View File

@ -5,18 +5,18 @@ description: >
entry in a list is an _identifier_ of some auth token. Example:
entries:
keycloak:
test.rkbmoney.keycloak:
- "token_a"
- "token_b"
apikeymgmt:
test.rkbmoney.apikeymgmt:
- "token_c"
Broadly speaking, what constitutes an _identifier_ depends on which _tokens_
are we talking about. Though for the foreseeable future, we consider only
JWTs where JWT's identifier is the value of the 'jti' claim.
entries:
keycloak:
test.rkbmoney.keycloak:
- "token_a"
- "token_b"
apikeymgmt:
test.rkbmoney.apikeymgmt:
- "token_c"

View File

@ -19,8 +19,8 @@ services:
image: dr2.rbkmoney.com/rbkmoney/machinegun:9c3248a68fe530d23a8266057a40a1a339a161b8
command: /opt/machinegun/bin/machinegun foreground
volumes:
- ./test/machinegun/config.yaml:/opt/machinegun/etc/config.yaml
- ./test/machinegun/cookie:/opt/machinegun/etc/cookie
- ./var/machinegun/config.yaml:/opt/machinegun/etc/config.yaml
- ./var/machinegun/cookie:/opt/machinegun/etc/cookie
healthcheck:
test: "curl http://localhost:8022/"
interval: 5s

63
elvis.config Normal file
View File

@ -0,0 +1,63 @@
[
{elvis, [
{config, [
#{
dirs => ["src"],
filter => "*.erl",
ruleset => erl_files,
rules => [
{elvis_text_style, line_length, #{limit => 120, skip_comments => false}},
% Too opinionated
{elvis_style, state_record_and_type, disable},
{elvis_style, invalid_dynamic_call, #{
ignore => [
% Implements parts of logger duties, including message formatting.
tk_audit_log
]
}}
]
},
#{
dirs => ["test"],
filter => "*.erl",
ruleset => erl_files,
rules => [
{elvis_text_style, line_length, #{limit => 120, skip_comments => false}},
% We want to use `ct:pal/2` and friends in test code.
{elvis_style, no_debug_call, disable},
% Assert macros can trigger use of ignored binding, yet we want them for better
% readability.
{elvis_style, used_ignored_variable, disable},
% Tests are usually more comprehensible when a bit more verbose.
{elvis_style, dont_repeat_yourself, #{min_complexity => 30}},
% Too opionated
{elvis_style, state_record_and_type, disable},
{elvis_style, god_modules, disable}
]
},
#{
dirs => ["."],
filter => "Makefile",
ruleset => makefiles
},
#{
dirs => ["."],
filter => "rebar.config",
rules => [
{elvis_text_style, line_length, #{limit => 120, skip_comments => false}},
{elvis_text_style, no_tabs},
{elvis_text_style, no_trailing_whitespace}
]
},
#{
dirs => ["src"],
filter => "*.app.src",
rules => [
{elvis_text_style, line_length, #{limit => 120, skip_comments => false}},
{elvis_text_style, no_tabs},
{elvis_text_style, no_trailing_whitespace}
]
}
]}
]}
].

View File

@ -1,6 +1,5 @@
%% Common project erlang options.
{erl_opts, [
% mandatory
debug_info,
warnings_as_errors,
@ -27,135 +26,35 @@
%% Common project dependencies.
{deps, [
{jsx, "3.0.0"},
{jose, "1.11.1"},
{jsx, "3.1.0"},
{jose, "1.11.2"},
{yamerl, "0.8.1"},
{thrift,
{git, "https://github.com/rbkmoney/thrift_erlang.git",
{branch, "master"}
}
},
{genlib,
{git, "https://github.com/rbkmoney/genlib.git",
{branch, "master"}
}
},
{thrift, {git, "https://github.com/rbkmoney/thrift_erlang.git", {branch, "master"}}},
{genlib, {git, "https://github.com/rbkmoney/genlib.git", {branch, "master"}}},
{snowflake, {git, "https://github.com/rbkmoney/snowflake.git", {branch, "master"}}},
{woody,
{git, "https://github.com/rbkmoney/woody_erlang.git",
{branch, "master"}
}
},
{woody_user_identity,
{git, "https://github.com/rbkmoney/woody_erlang_user_identity.git",
{branch, "master"}
}
},
{token_keeper_proto,
{git, "https://github.com/rbkmoney/token-keeper-proto.git",
{branch, "master"}
}
},
{scoper,
{git, "https://github.com/rbkmoney/scoper.git",
{branch, "master"}
}
},
{erl_health,
{git, "https://github.com/rbkmoney/erlang-health.git",
{branch, "master"}
}
},
{bouncer_client,
{git, "https://github.com/rbkmoney/bouncer_client_erlang.git",
{branch, master}
}
},
{woody, {git, "https://github.com/rbkmoney/woody_erlang.git", {branch, "master"}}},
{woody_user_identity, {git, "https://github.com/rbkmoney/woody_erlang_user_identity.git", {branch, "master"}}},
{token_keeper_proto, {git, "https://github.com/rbkmoney/token-keeper-proto.git", {branch, "master"}}},
{scoper, {git, "https://github.com/rbkmoney/scoper.git", {branch, "master"}}},
{erl_health, {git, "https://github.com/rbkmoney/erlang-health.git", {branch, "master"}}},
%% Only needed for some utility functions
{bouncer_client, {git, "https://github.com/rbkmoney/bouncer_client_erlang.git", {branch, master}}},
{machinery, {git, "https://github.com/rbkmoney/machinery.git", {branch, "master"}}},
% Production-only deps.
% Defined here for the sake of rebar-locking.
{recon, "2.5.1"},
{logger_logstash_formatter,
{git, "https://github.com/rbkmoney/logger_logstash_formatter.git",
{branch, "master"}
}
},
{how_are_you,
{git, "https://github.com/rbkmoney/how_are_you.git",
{branch, "master"}
}
}
{recon, "2.5.2"},
{logger_logstash_formatter, {git, "https://github.com/rbkmoney/logger_logstash_formatter.git", {branch, "master"}}},
{how_are_you, {git, "https://github.com/rbkmoney/how_are_you.git", {branch, "master"}}}
]}.
%% Helpful plugins.
{plugins, [
{rebar3_lint, "0.3.0"},
{erlfmt, "0.10.0"}
{rebar3_lint, "1.0.1"},
{erlfmt, "1.0.0"}
]}.
%% Linter config.
{elvis, [
#{
dirs => ["src"],
filter => "*.erl",
ruleset => erl_files,
rules => [
{elvis_text_style, line_length, #{limit => 120, skip_comments => false}},
% Too opinionated
{elvis_style, state_record_and_type, disable},
{elvis_style, invalid_dynamic_call, #{
ignore => [
% Implements parts of logger duties, including message formatting.
tk_audit_log
]
}}
]
},
#{
dirs => ["test"],
filter => "*.erl",
ruleset => erl_files,
rules => [
{elvis_text_style, line_length, #{limit => 120, skip_comments => false}},
% We want to use `ct:pal/2` and friends in test code.
{elvis_style, no_debug_call, disable},
% Assert macros can trigger use of ignored binding, yet we want them for better
% readability.
{elvis_style, used_ignored_variable, disable},
% Tests are usually more comprehensible when a bit more verbose.
{elvis_style, dont_repeat_yourself, #{min_complexity => 20}},
% Too opionated
{elvis_style, state_record_and_type, disable},
{elvis_style, god_modules, disable}
]
},
#{
dirs => ["."],
filter => "Makefile",
ruleset => makefiles
},
#{
dirs => ["."],
filter => "rebar.config",
rules => [
{elvis_text_style, line_length, #{limit => 120, skip_comments => false}},
{elvis_text_style, no_tabs},
{elvis_text_style, no_trailing_whitespace}
]
},
#{
dirs => ["src"],
filter => "*.app.src",
rules => [
{elvis_text_style, line_length, #{limit => 120, skip_comments => false}},
{elvis_text_style, no_tabs},
{elvis_text_style, no_trailing_whitespace}
]
}
]}.
{elvis_output_format, colors}.
%% XRef checks
@ -187,11 +86,15 @@
{prod, [
%% Relx configuration
{relx, [
{release, {token_keeper , "0.1.0"}, [
{recon , load}, % tools for introspection
{runtime_tools, load}, % debugger
{tools , load}, % profiler
{logger_logstash_formatter, load}, % logger formatter
{release, {token_keeper, "0.1.0"}, [
% tools for introspection
{recon, load},
% debugger
{runtime_tools, load},
% profiler
{tools, load},
% logger formatter
{logger_logstash_formatter, load},
how_are_you,
token_keeper
]},
@ -208,11 +111,11 @@
]}.
{shell, [
% {config, "config/sys.config"},
% {config, "config/sys.config"},
{apps, [token_keeper]}
]}.
{erlfmt, [
{print_width, 120},
{files, "{src,test}/*.{hrl,erl,src}"}
{files, ["{src,test}/*.{hrl,erl,app.src}", "rebar.config", "elvis.config", "config/sys.config"]}
]}.

View File

@ -1,45 +1,45 @@
{"1.2.0",
[{<<"bear">>,{pkg,<<"bear">>,<<"0.8.7">>},2},
[{<<"bear">>,{pkg,<<"bear">>,<<"0.9.0">>},2},
{<<"bouncer_client">>,
{git,"https://github.com/rbkmoney/bouncer_client_erlang.git",
{ref,"36cb53a7d4fea4861d5ea5cf7e2f572eba941fde"}},
{ref,"535449a459b70643836c440a863b42656f2a1409"}},
0},
{<<"bouncer_proto">>,
{git,"git@github.com:rbkmoney/bouncer-proto.git",
{ref,"7ac88717904c6bab73096198b308380e006ed42c"}},
{git,"https://github.com/rbkmoney/bouncer-proto",
{ref,"8da12fe98bc751e7f8f17f64ad4f571a6a63b0fe"}},
1},
{<<"cache">>,{pkg,<<"cache">>,<<"2.3.3">>},1},
{<<"certifi">>,{pkg,<<"certifi">>,<<"2.5.3">>},2},
{<<"certifi">>,{pkg,<<"certifi">>,<<"2.8.0">>},2},
{<<"cg_mon">>,
{git,"https://github.com/rbkmoney/cg_mon.git",
{ref,"5a87a37694e42b6592d3b4164ae54e0e87e24e18"}},
1},
{<<"cowboy">>,{pkg,<<"cowboy">>,<<"2.8.0">>},1},
{<<"cowlib">>,{pkg,<<"cowlib">>,<<"2.9.1">>},2},
{<<"cowboy">>,{pkg,<<"cowboy">>,<<"2.9.0">>},1},
{<<"cowlib">>,{pkg,<<"cowlib">>,<<"2.11.0">>},2},
{<<"erl_health">>,
{git,"https://github.com/rbkmoney/erlang-health.git",
{ref,"982af88738ca062eea451436d830eef8c1fbe3f9"}},
{ref,"5958e2f35cd4d09f40685762b82b82f89b4d9333"}},
0},
{<<"folsom">>,
{git,"https://github.com/folsom-project/folsom.git",
{ref,"eeb1cc467eb64bd94075b95b8963e80d8b4df3df"}},
{ref,"62fd0714e6f0b4e7833880afe371a9c882ea0fc2"}},
1},
{<<"genlib">>,
{git,"https://github.com/rbkmoney/genlib.git",
{ref,"4565a8d73f34a0b78cca32c9cd2b97d298bdadf8"}},
{ref,"82c5ff3866e3019eb347c7f1d8f1f847bed28c10"}},
0},
{<<"gproc">>,{pkg,<<"gproc">>,<<"0.8.0">>},1},
{<<"hackney">>,{pkg,<<"hackney">>,<<"1.17.0">>},1},
{<<"gproc">>,{pkg,<<"gproc">>,<<"0.9.0">>},1},
{<<"hackney">>,{pkg,<<"hackney">>,<<"1.18.0">>},1},
{<<"how_are_you">>,
{git,"https://github.com/rbkmoney/how_are_you.git",
{ref,"29f9d3d7c35f7a2d586c8571f572838df5ec91dd"}},
{ref,"2fd8013420328464c2c84302af2781b86577b39f"}},
0},
{<<"idna">>,{pkg,<<"idna">>,<<"6.1.1">>},2},
{<<"jose">>,{pkg,<<"jose">>,<<"1.11.1">>},0},
{<<"jsx">>,{pkg,<<"jsx">>,<<"3.0.0">>},0},
{<<"jose">>,{pkg,<<"jose">>,<<"1.11.2">>},0},
{<<"jsx">>,{pkg,<<"jsx">>,<<"3.1.0">>},0},
{<<"logger_logstash_formatter">>,
{git,"https://github.com/rbkmoney/logger_logstash_formatter.git",
{ref,"87e52c755cf9e64d651e3ddddbfcd2ccd1db79db"}},
{ref,"2c7b71630527a932f2a1aef4edcec66863c1367a"}},
0},
{<<"machinery">>,
{git,"https://github.com/rbkmoney/machinery.git",
@ -56,11 +56,11 @@
{ref,"06c5c8430e445cb7874e54358e457cbb5697fc32"}},
1},
{<<"parse_trans">>,{pkg,<<"parse_trans">>,<<"3.3.1">>},2},
{<<"ranch">>,{pkg,<<"ranch">>,<<"1.7.1">>},2},
{<<"recon">>,{pkg,<<"recon">>,<<"2.5.1">>},0},
{<<"ranch">>,{pkg,<<"ranch">>,<<"1.8.0">>},2},
{<<"recon">>,{pkg,<<"recon">>,<<"2.5.2">>},0},
{<<"scoper">>,
{git,"https://github.com/rbkmoney/scoper.git",
{ref,"89a973bf3cedc5a48c9fd89d719d25e79fe10027"}},
{ref,"7f3183df279bc8181efe58dafd9cae164f495e6f"}},
0},
{<<"snowflake">>,
{git,"https://github.com/rbkmoney/snowflake.git",
@ -69,16 +69,16 @@
{<<"ssl_verify_fun">>,{pkg,<<"ssl_verify_fun">>,<<"1.1.6">>},2},
{<<"thrift">>,
{git,"https://github.com/rbkmoney/thrift_erlang.git",
{ref,"846a0819d9b6d09d0c31f160e33a78dbad2067b4"}},
{ref,"c280ff266ae1c1906fb0dcee8320bb8d8a4a3c75"}},
0},
{<<"token_keeper_proto">>,
{git,"https://github.com/rbkmoney/token-keeper-proto.git",
{ref,"c7f48d24a561c95b8135d3b07fd2ff55a62eb308"}},
{ref,"8f7016f68692fc8e3141ba0fce2d47b6c8b6102a"}},
0},
{<<"unicode_util_compat">>,{pkg,<<"unicode_util_compat">>,<<"0.7.0">>},2},
{<<"woody">>,
{git,"https://github.com/rbkmoney/woody_erlang.git",
{ref,"f2cd30883d58eb1c3ab2172556956f757bc27e23"}},
{ref,"6f818c57e3b19f96260b1f968115c9bc5bcad4d2"}},
0},
{<<"woody_user_identity">>,
{git,"https://github.com/rbkmoney/woody_erlang_user_identity.git",
@ -87,40 +87,40 @@
{<<"yamerl">>,{pkg,<<"yamerl">>,<<"0.8.1">>},0}]}.
[
{pkg_hash,[
{<<"bear">>, <<"16264309AE5D005D03718A5C82641FCC259C9E8F09ADEB6FD79CA4271168656F">>},
{<<"bear">>, <<"A31CCF5361791DD5E708F4789D67E2FEF496C4F05935FC59ADC11622F834D128">>},
{<<"cache">>, <<"B23A5FE7095445A88412A6E614C933377E0137B44FFED77C9B3FEF1A731A20B2">>},
{<<"certifi">>, <<"70BDD7E7188C804F3A30EE0E7C99655BC35D8AC41C23E12325F36AB449B70651">>},
{<<"cowboy">>, <<"F3DC62E35797ECD9AC1B50DB74611193C29815401E53BAC9A5C0577BD7BC667D">>},
{<<"cowlib">>, <<"61A6C7C50CF07FDD24B2F45B89500BB93B6686579B069A89F88CB211E1125C78">>},
{<<"gproc">>, <<"CEA02C578589C61E5341FCE149EA36CCEF236CC2ECAC8691FBA408E7EA77EC2F">>},
{<<"hackney">>, <<"717EA195FD2F898D9FE9F1CE0AFCC2621A41ECFE137FAE57E7FE6E9484B9AA99">>},
{<<"certifi">>, <<"D4FB0A6BB20B7C9C3643E22507E42F356AC090A1DCEA9AB99E27E0376D695EBA">>},
{<<"cowboy">>, <<"865DD8B6607E14CF03282E10E934023A1BD8BE6F6BACF921A7E2A96D800CD452">>},
{<<"cowlib">>, <<"0B9FF9C346629256C42EBE1EEB769A83C6CB771A6EE5960BD110AB0B9B872063">>},
{<<"gproc">>, <<"853CCB7805E9ADA25D227A157BA966F7B34508F386A3E7E21992B1B484230699">>},
{<<"hackney">>, <<"C4443D960BB9FBA6D01161D01CD81173089686717D9490E5D3606644C48D121F">>},
{<<"idna">>, <<"8A63070E9F7D0C62EB9D9FCB360A7DE382448200FBBD1B106CC96D3D8099DF8D">>},
{<<"jose">>, <<"59DA64010C69AAD6CDE2F5B9248B896B84472E99BD18F246085B7B9FE435DCDB">>},
{<<"jsx">>, <<"20A170ABD4335FC6DB24D5FAD1E5D677C55DADF83D1B20A8A33B5FE159892A39">>},
{<<"jose">>, <<"F4C018CCF4FDCE22C71E44D471F15F723CB3EFAB5D909AB2BA202B5BF35557B3">>},
{<<"jsx">>, <<"D12516BAA0BB23A59BB35DCCAF02A1BD08243FCBB9EFE24F2D9D056CCFF71268">>},
{<<"metrics">>, <<"25F094DEA2CDA98213CECC3AEFF09E940299D950904393B2A29D191C346A8486">>},
{<<"mimerl">>, <<"67E2D3F571088D5CFD3E550C383094B47159F3EEE8FFA08E64106CDF5E981BE3">>},
{<<"parse_trans">>, <<"16328AB840CC09919BD10DAB29E431DA3AF9E9E7E7E6F0089DD5A2D2820011D8">>},
{<<"ranch">>, <<"6B1FAB51B49196860B733A49C07604465A47BDB78AA10C1C16A3D199F7F8C881">>},
{<<"recon">>, <<"430FFA60685AC1EFDFB1FE4C97B8767C92D0D92E6E7C3E8621559BA77598678A">>},
{<<"ranch">>, <<"8C7A100A139FD57F17327B6413E4167AC559FBC04CA7448E9BE9057311597A1D">>},
{<<"recon">>, <<"CBA53FA8DB83AD968C9A652E09C3ED7DDCC4DA434F27C3EAA9CA47FFB2B1FF03">>},
{<<"ssl_verify_fun">>, <<"CF344F5692C82D2CD7554F5EC8FD961548D4FD09E7D22F5B62482E5AEAEBD4B0">>},
{<<"unicode_util_compat">>, <<"BC84380C9AB48177092F43AC89E4DFA2C6D62B40B8BD132B1059ECC7232F9A78">>},
{<<"yamerl">>, <<"07DA13FFA1D8E13948943789665C62CCD679DFA7B324A4A2ED3149DF17F453A4">>}]},
{pkg_hash_ext,[
{<<"bear">>, <<"534217DCE6A719D59E54FB0EB7A367900DBFC5F85757E8C1F94269DF383F6D9B">>},
{<<"bear">>, <<"47F71F098F2E3CD05E124A896C5EC2F155967A2B6FF6731E0D627312CCAB7E28">>},
{<<"cache">>, <<"44516CE6FA03594D3A2AF025DD3A87BFE711000EB730219E1DDEFC816E0AA2F4">>},
{<<"certifi">>, <<"ED516ACB3929B101208A9D700062D520F3953DA3B6B918D866106FFA980E1C10">>},
{<<"cowboy">>, <<"4643E4FBA74AC96D4D152C75803DE6FAD0B3FA5DF354C71AFDD6CBEEB15FAC8A">>},
{<<"cowlib">>, <<"E4175DC240A70D996156160891E1C62238EDE1729E45740BDD38064DAD476170">>},
{<<"gproc">>, <<"580ADAFA56463B75263EF5A5DF4C86AF321F68694E7786CB057FD805D1E2A7DE">>},
{<<"hackney">>, <<"64C22225F1EA8855F584720C0E5B3CD14095703AF1C9FBC845BA042811DC671C">>},
{<<"certifi">>, <<"6AC7EFC1C6F8600B08D625292D4BBF584E14847CE1B6B5C44D983D273E1097EA">>},
{<<"cowboy">>, <<"2C729F934B4E1AA149AFF882F57C6372C15399A20D54F65C8D67BEF583021BDE">>},
{<<"cowlib">>, <<"2B3E9DA0B21C4565751A6D4901C20D1B4CC25CBB7FD50D91D2AB6DD287BC86A9">>},
{<<"gproc">>, <<"587E8AF698CCD3504CF4BA8D90F893EDE2B0F58CABB8A916E2BF9321DE3CF10B">>},
{<<"hackney">>, <<"9AFCDA620704D720DB8C6A3123E9848D09C87586DC1C10479C42627B905B5C5E">>},
{<<"idna">>, <<"92376EB7894412ED19AC475E4A86F7B413C1B9FBB5BD16DCCD57934157944CEA">>},
{<<"jose">>, <<"078F6C9FB3CD2F4CFAFC972C814261A7D1E8D2B3685C0A76EB87E158EFFF1AC5">>},
{<<"jsx">>, <<"37BECA0435F5CA8A2F45F76A46211E76418FBEF80C36F0361C249FC75059DC6D">>},
{<<"jose">>, <<"98143FBC48D55F3A18DABA82D34FE48959D44538E9697C08F34200FA5F0947D2">>},
{<<"jsx">>, <<"0C5CC8FDC11B53CC25CF65AC6705AD39E54ECC56D1C22E4ADB8F5A53FB9427F3">>},
{<<"metrics">>, <<"69B09ADDDC4F74A40716AE54D140F93BEB0FB8978D8636EADED0C31B6F099F16">>},
{<<"mimerl">>, <<"F278585650AA581986264638EBF698F8BB19DF297F66AD91B18910DFC6E19323">>},
{<<"parse_trans">>, <<"07CD9577885F56362D414E8C4C4E6BDF10D43A8767ABB92D24CBE8B24C54888B">>},
{<<"ranch">>, <<"451D8527787DF716D99DC36162FCA05934915DB0B6141BBDAC2EA8D3C7AFC7D7">>},
{<<"recon">>, <<"5721C6B6D50122D8F68CCCAC712CAA1231F97894BAB779EFF5FF0F886CB44648">>},
{<<"ranch">>, <<"49FBCFD3682FAB1F5D109351B61257676DA1A2FDBE295904176D5E521A2DDFE5">>},
{<<"recon">>, <<"2C7523C8DEE91DFF41F6B3D63CBA2BD49EB6D2FE5BF1EEC0DF7F87EB5E230E1C">>},
{<<"ssl_verify_fun">>, <<"BDB0D2471F453C88FF3908E7686F86F9BE327D065CC1EC16FA4540197EA04680">>},
{<<"unicode_util_compat">>, <<"25EEE6D67DF61960CF6A794239566599B09E17E668D3700247BC498638152521">>},
{<<"yamerl">>, <<"96CB30F9D64344FED0EF8A92E9F16F207DE6C04DFFF4F366752CA79F5BCEB23F">>}]}

View File

@ -233,12 +233,15 @@ log_allowed(Level) ->
%%
get_level({get_by_token, started}, _Level) -> log_allowed(debug);
get_level({create_ephemeral, started}, _Level) -> log_allowed(debug);
get_level({authenticate, started}, _Level) -> log_allowed(debug);
get_level(_, Level) -> Level.
get_message({Op, {failed, _}}) -> get_message({Op, failed});
get_message({Op, Event}) -> iolist_to_binary([atom_to_binary(Op), <<" ">>, atom_to_binary(Event)]).
get_message({Op, {failed, _}}) ->
get_message({Op, failed});
get_message({Op, Event}) ->
EncodedOp = iolist_to_binary(encode_op(Op)),
EncodedEvent = atom_to_binary(Event),
<<EncodedOp/binary, " ", EncodedEvent/binary>>.
get_beat_metadata({Op, Event}) ->
#{Op => build_event(Event)}.
@ -251,6 +254,11 @@ build_event({failed, Error}) ->
build_event(Event) ->
#{event => Event}.
encode_op(Op) when is_atom(Op) ->
[atom_to_binary(Op)];
encode_op({Namespace, Sub}) ->
[atom_to_binary(Namespace), <<":">> | encode_op(Sub)].
encode_error({Class, Details}) when is_atom(Class) ->
#{class => Class, details => genlib:format(Details)};
encode_error(Class) when is_atom(Class) ->
@ -261,7 +269,9 @@ encode_error(Other) ->
extract_metadata(Metadata, Acc) ->
Acc1 = extract_opt_meta(token, Metadata, fun encode_token/1, Acc),
Acc2 = extract_opt_meta(source, Metadata, fun encode_token_source/1, Acc1),
extract_woody_ctx(maps:get(woody_ctx, Metadata, undefined), Acc2).
Acc3 = extract_opt_meta(authority_id, Metadata, fun encode_authority_id/1, Acc2),
Acc4 = extract_opt_meta(authdata_id, Metadata, fun encode_authdata_id/1, Acc3),
extract_woody_ctx(maps:get(woody_ctx, Metadata, undefined), Acc4).
extract_opt_meta(K, Metadata, EncodeFun, Acc) ->
case maps:find(K, Metadata) of
@ -271,15 +281,19 @@ extract_opt_meta(K, Metadata, EncodeFun, Acc) ->
encode_token(TokenInfo) ->
#{
jti => tk_token_jwt:get_token_id(TokenInfo),
claims => tk_token_jwt:get_claims(TokenInfo),
authority => tk_token_jwt:get_authority(TokenInfo),
metadata => tk_token_jwt:get_metadata(TokenInfo)
jti => maps:get(id, TokenInfo),
payload => maps:get(payload, TokenInfo)
}.
encode_token_source(TokenSourceContext = #{}) ->
TokenSourceContext.
encode_authority_id(AuthorityID) when is_binary(AuthorityID) ->
AuthorityID.
encode_authdata_id(AuthDataID) when is_binary(AuthDataID) ->
AuthDataID.
extract_woody_ctx(WoodyCtx = #{rpc_id := RpcID}, Acc) ->
extract_woody_meta(WoodyCtx, extract_woody_rpc_id(RpcID, Acc));
extract_woody_ctx(undefined, Acc) ->

44
src/tk_authdata.erl Normal file
View File

@ -0,0 +1,44 @@
-module(tk_authdata).
-export([create_prototype/3]).
%%
-type prototype() :: #{
id => id(),
status := status(),
context := encoded_context_fragment(),
authority => authority_id(),
metadata => metadata()
}.
-type id() :: binary().
-type status() :: active | revoked.
-type encoded_context_fragment() :: tk_context_thrift:'ContextFragment'().
-type authority_id() :: binary().
-type metadata() :: #{binary() => binary()}.
-export_type([prototype/0]).
-export_type([id/0]).
-export_type([status/0]).
-export_type([authority_id/0]).
-export_type([encoded_context_fragment/0]).
-export_type([metadata/0]).
%%
-spec create_prototype(id() | undefined, encoded_context_fragment(), metadata()) -> prototype().
create_prototype(ID, ContextFragment, Metadata) ->
AuthData = #{
status => active,
context => ContextFragment,
metadata => Metadata
},
add_id(AuthData, ID).
%%
add_id(AuthData, undefined) ->
AuthData;
add_id(AuthData, ID) ->
AuthData#{id => ID}.

View File

@ -2,8 +2,7 @@
%% Behaviour
-callback get_authdata(tk_token_jwt:t(), source_opts(), tk_woody_handler:handle_ctx()) ->
sourced_authdata() | undefined.
-callback get_authdata(tk_token:token_data(), opts(), woody_context:ctx()) -> authdata() | undefined.
%% API functions
@ -11,50 +10,42 @@
%% API Types
-type authdata_source() :: storage_source() | claim_source() | extractor_source().
-type sourced_authdata() :: #{
id => tk_authority:authdata_id(),
status := tk_authority:status(),
context := tk_authority:encoded_context_fragment(),
authority => tk_authority:autority_id(),
metadata => tk_authority:metadata()
}.
-type authdata_source() :: claim_source() | storage_source() | legacy_claim_source() | extractor_source().
-type source_opts() ::
tk_authdata_source_extractor:source_opts()
| tk_authdata_source_claim:source_opts()
| tk_authdata_source_storage:source_opts().
-type opts() ::
tk_authdata_source_claim:opts()
| tk_authdata_source_storage:opts()
| tk_authdata_source_context_extractor:opts()
| tk_authdata_source_legacy_claim:opts().
-export_type([authdata_source/0]).
-export_type([sourced_authdata/0]).
%% Internal types
-type storage_source() :: {storage, tk_authdata_source_storage:source_opts()}.
-type claim_source() :: {claim, tk_authdata_source_claim:source_opts()}.
-type extractor_source() :: maybe_opts(extractor, tk_authdata_source_extractor:source_opts()).
-type authdata() :: tk_authdata:prototype().
-type maybe_opts(Source, Opts) :: Source | {Source, Opts}.
-type claim_source() :: {claim, tk_authdata_source_claim:opts()}.
-type storage_source() :: {storage, tk_authdata_source_storage:opts()}.
-type legacy_claim_source() :: {legacy_claim, tk_authdata_source_legacy_claim:opts()}.
-type extractor_source() :: {extract_context, tk_authdata_source_context_extractor:opts()}.
%% API functions
-spec get_authdata(authdata_source(), tk_token_jwt:t(), tk_woody_handler:handle_ctx()) ->
sourced_authdata() | undefined.
get_authdata(AuthDataSource, Token, Ctx) ->
{Source, Opts} = get_source_opts(AuthDataSource),
Hander = get_source_handler(Source),
Hander:get_authdata(Token, Opts, Ctx).
-spec get_authdata(tk_token:token_data(), authdata_source(), woody_context:ctx()) -> authdata() | undefined.
get_authdata(TokenPayload, AuthdataSource, Context) ->
{Handler, Opts} = get_source_modopts(AuthdataSource),
Handler:get_authdata(TokenPayload, Opts, Context).
%%
%% Internal functions
get_source_opts({_Source, _Opts} = StorageOpts) ->
StorageOpts;
get_source_opts(Source) when is_atom(Source) ->
{Source, #{}}.
get_source_modopts({SourceType, Opts}) ->
{get_source_handler(SourceType), Opts}.
get_source_handler(storage) ->
tk_authdata_source_storage;
get_source_handler(claim) ->
tk_authdata_source_claim;
get_source_handler(extract) ->
tk_authdata_source_extractor.
get_source_handler(storage) ->
tk_authdata_source_storage;
get_source_handler(legacy_claim) ->
tk_authdata_source_legacy_claim;
get_source_handler(extract_context) ->
tk_authdata_source_context_extractor.

View File

@ -5,23 +5,23 @@
-export([get_authdata/3]).
%%
%% API types
-type stored_authdata() :: tk_storage:storable_authdata().
-type source_opts() :: tk_token_claim_utils:decode_opts().
-type opts() :: #{}.
-export_type([opts/0]).
-export_type([stored_authdata/0]).
-export_type([source_opts/0]).
%% Internal types
-type authdata() :: tk_authdata:prototype().
%% Behaviour functions
-spec get_authdata(tk_token_jwt:t(), source_opts(), tk_woody_handler:handle_ctx()) -> stored_authdata() | undefined.
get_authdata(Token, Opts, _Ctx) ->
Claims = tk_token_jwt:get_claims(Token),
case tk_token_claim_utils:decode_authdata(Claims, Opts) of
-spec get_authdata(tk_token:token_data(), opts(), woody_context:ctx()) -> authdata() | undefined.
get_authdata(#{payload := TokenPayload}, _Opts, _Context) ->
case tk_claim_utils:decode_authdata(TokenPayload) of
{ok, AuthData} ->
AuthData;
{error, Reason} ->
_ = logger:warning("Failed claim get: ~p", [Reason]),
_ = logger:warning("Failed attempt to decode bouncer context from claims: ~p", [Reason]),
undefined
end.

View File

@ -1,4 +1,4 @@
-module(tk_authdata_source_extractor).
-module(tk_authdata_source_context_extractor).
-behaviour(tk_authdata_source).
-include_lib("token_keeper_proto/include/tk_context_thrift.hrl").
@ -7,47 +7,43 @@
-export([get_authdata/3]).
%%
%% API types
-type extracted_authdata() :: #{
status := tk_authority:status(),
context := tk_authority:encoded_context_fragment(),
metadata => tk_authority:metadata()
-type opts() :: #{
methods => tk_context_extractor:methods()
}.
-type source_opts() :: #{
methods => tk_extractor:methods()
}.
-export_type([opts/0]).
-export_type([extracted_authdata/0]).
-export_type([source_opts/0]).
%% Internal types
-type authdata() :: tk_authdata:prototype().
%% Behaviour functions
-spec get_authdata(tk_token_jwt:t(), source_opts(), tk_woody_handler:handle_ctx()) -> extracted_authdata() | undefined.
get_authdata(Token, Opts, _Ctx) ->
-spec get_authdata(tk_token:token_data(), opts(), woody_context:ctx()) -> authdata() | undefined.
get_authdata(VerifiedToken, Opts, _Context) ->
Methods = get_extractor_methods(Opts),
case extract_context_with(Methods, Token) of
case extract_context_with(Methods, VerifiedToken) of
{Context, Metadata} ->
make_auth_data(Context, Metadata);
undefined ->
undefined
end.
%%
%% Internal functions
get_extractor_methods(Opts) ->
maps:get(methods, Opts).
extract_context_with([], _Token) ->
extract_context_with([], _VerifiedToken) ->
undefined;
extract_context_with([MethodOpts | Rest], Token) ->
{Method, Opts} = get_method_opts(MethodOpts),
case tk_extractor:get_context(Method, Token, Opts) of
extract_context_with([MethodOpts | Rest], VerifiedToken) ->
case tk_context_extractor:extract_context(MethodOpts, VerifiedToken) of
AuthData when AuthData =/= undefined ->
AuthData;
undefined ->
extract_context_with(Rest, Token)
extract_context_with(Rest, VerifiedToken)
end.
make_auth_data(ContextFragment, Metadata) ->
@ -70,8 +66,3 @@ encode_context_fragment_content(ContextFragment) ->
{ok, Codec1} ->
thrift_strict_binary_codec:close(Codec1)
end.
get_method_opts({_Method, _Opts} = MethodOpts) ->
MethodOpts;
get_method_opts(Method) when is_atom(Method) ->
{Method, #{}}.

View File

@ -0,0 +1,61 @@
-module(tk_authdata_source_legacy_claim).
-behaviour(tk_authdata_source).
%% Behaviour
-export([get_authdata/3]).
%% API types
-type opts() :: #{
metadata_mappings := #{
party_id := binary(),
token_consumer := binary()
}
}.
-export_type([opts/0]).
%% Internal types
-type authdata() :: tk_authdata:prototype().
%%
-define(CLAIM_BOUNCER_CTX, <<"bouncer_ctx">>).
-define(CLAIM_PARTY_ID, <<"sub">>).
-define(CLAIM_CONSUMER_TYPE, <<"cons">>).
%% Behaviour functions
-spec get_authdata(tk_token:token_data(), opts(), woody_context:ctx()) -> authdata() | undefined.
get_authdata(#{payload := TokenPayload}, Opts, _Context) ->
case decode_bouncer_claim(TokenPayload) of
{ok, ContextFragment} ->
create_authdata(ContextFragment, create_metadata(TokenPayload, Opts));
{error, Reason} ->
_ = logger:warning("Failed attempt to decode bouncer context from legacy claims: ~p", [Reason]),
undefined
end.
%%
decode_bouncer_claim(#{?CLAIM_BOUNCER_CTX := BouncerClaim}) ->
tk_claim_utils:decode_bouncer_claim(BouncerClaim);
decode_bouncer_claim(_Claims) ->
{error, bouncer_claim_not_found}.
create_authdata(ContextFragment, Metadata) ->
genlib_map:compact(#{
status => active,
context => ContextFragment,
metadata => Metadata
}).
create_metadata(TokenPayload, Opts) ->
Metadata = #{
%% TODO: This is a temporary hack.
%% When some external services will stop requiring woody user identity to be present it must be removed too
party_id => maps:get(?CLAIM_PARTY_ID, TokenPayload, undefined),
consumer => maps:get(?CLAIM_CONSUMER_TYPE, TokenPayload, undefined)
},
tk_utils:remap(genlib_map:compact(Metadata), maps:get(metadata_mappings, Opts)).

View File

@ -5,27 +5,25 @@
-export([get_authdata/3]).
%%
%% API types
-type stored_authdata() :: tk_storage:storable_authdata().
-type source_opts() :: #{}.
-type opts() :: #{
name := tk_storage:storage_name()
}.
-export_type([opts/0]).
-export_type([stored_authdata/0]).
-export_type([source_opts/0]).
%% Internal types
-type authdata() :: tk_authdata:prototype().
%% Behaviour functions
-spec get_authdata(tk_token_jwt:t(), source_opts(), tk_woody_handler:handle_ctx()) -> stored_authdata() | undefined.
get_authdata(Token, _SourceOpts, Ctx) ->
case tk_storage:get(get_authdata_id(Token), Ctx) of
-spec get_authdata(tk_token:token_data(), opts(), woody_context:ctx()) -> authdata() | undefined.
get_authdata(#{id := ID}, #{name := StorageName}, Context) ->
case tk_storage:get(ID, StorageName, Context) of
{ok, AuthData} ->
AuthData;
{error, Reason} ->
_ = logger:warning("Failed storage get: ~p", [Reason]),
_ = logger:warning("Failed attempt to get bouncer context from storage: ~p", [Reason]),
undefined
end.
%%
get_authdata_id(Claims) ->
tk_token_jwt:get_token_id(Claims).

View File

@ -1,115 +0,0 @@
-module(tk_authority).
-include_lib("token_keeper_proto/include/tk_context_thrift.hrl").
-include_lib("token_keeper_proto/include/tk_token_keeper_thrift.hrl").
%% API functions
-export([get_id/1]).
-export([get_authdata_id/1]).
-export([get_signer/1]).
-export([create_authdata/4]).
-export([get_authdata_by_token/3]).
%% API Types
-type authority() :: #{
id := autority_id(),
signer => tk_token_jwt:keyname(),
authdata_sources := authdata_sources()
}.
-type authdata_sources() :: [tk_authdata_source:authdata_source()].
-type autority_id() :: binary().
-type authdata() :: #{
id => authdata_id(),
status := status(),
context := encoded_context_fragment(),
authority := autority_id(),
metadata => metadata()
}.
-type authdata_id() :: binary().
-type status() :: active | revoked.
-type encoded_context_fragment() :: tk_context_thrift:'ContextFragment'().
-type metadata() :: #{binary() => binary()}.
-export_type([authority/0]).
-export_type([authdata/0]).
-export_type([authdata_id/0]).
-export_type([status/0]).
-export_type([encoded_context_fragment/0]).
-export_type([metadata/0]).
-export_type([autority_id/0]).
%% API Functions
-spec get_id(authority()) -> autority_id().
get_id(Authority) ->
maps:get(id, Authority).
-spec get_authdata_id(authdata()) -> authdata_id().
get_authdata_id(AuthData) ->
maps:get(id, AuthData).
-spec get_signer(authority()) -> tk_token_jwt:keyname().
get_signer(Authority) ->
maps:get(signer, Authority).
-spec create_authdata(authdata_id() | undefined, encoded_context_fragment(), metadata(), authority()) -> authdata().
create_authdata(ID, ContextFragment, Metadata, Authority) ->
AuthData = #{
status => active,
context => ContextFragment,
metadata => Metadata
},
add_authority_id(add_id(AuthData, ID), Authority).
-spec get_authdata_by_token(tk_token_jwt:t(), authority(), tk_woody_handler:handle_ctx()) ->
{ok, authdata()} | {error, {authdata_not_found, _Sources}}.
get_authdata_by_token(Token, Authority, Ctx) ->
AuthDataSources = get_auth_data_sources(Authority),
case get_authdata_from_sources(AuthDataSources, Token, Ctx) of
#{} = AuthData ->
{ok, maybe_add_authority_id(AuthData, Authority)};
undefined ->
{error, {authdata_not_found, AuthDataSources}}
end.
%%-------------------------------------
%% private functions
-spec get_auth_data_sources(authority()) -> authdata_sources().
get_auth_data_sources(Authority) ->
case maps:get(authdata_sources, Authority, undefined) of
Sources when is_list(Sources) ->
Sources;
undefined ->
throw({misconfiguration, {no_authdata_sources, Authority}})
end.
get_authdata_from_sources([], _Token, _Ctx) ->
undefined;
get_authdata_from_sources([SourceOpts | Rest], Token, Ctx) ->
case tk_authdata_source:get_authdata(SourceOpts, Token, Ctx) of
undefined ->
get_authdata_from_sources(Rest, Token, Ctx);
AuthData ->
AuthData
end.
maybe_add_authority_id(AuthData = #{authority := _}, _Authority) ->
AuthData;
maybe_add_authority_id(AuthData, Authority) ->
add_authority_id(AuthData, Authority).
add_id(AuthData, undefined) ->
AuthData;
add_id(AuthData, ID) ->
AuthData#{id => ID}.
add_authority_id(AuthData, Authority) when is_map(Authority) ->
AuthData#{authority => maps:get(id, Authority)}.

View File

@ -1,4 +1,4 @@
-module(tk_token_blacklist).
-module(tk_blacklist).
-behaviour(supervisor).
@ -22,8 +22,7 @@
%%
-define(APP, token_keeper).
-define(TERM_KEY, {?MODULE, mappings}).
-define(TAB, ?MODULE).
%%
@ -35,32 +34,26 @@ child_spec(Options) ->
type => supervisor
}.
-spec is_blacklisted(binary(), atom()) -> boolean().
is_blacklisted(Token, AuthorityID) ->
match_entry(AuthorityID, Token, get_entires()).
%%
match_entry(AuthorityID, Token, Entries) ->
case maps:get(AuthorityID, Entries, undefined) of
AuthorityEntries when AuthorityEntries =/= undefined ->
lists:member(Token, AuthorityEntries);
undefined ->
false
end.
-spec is_blacklisted(tk_token:token_id(), tk_authdata:authority_id()) -> boolean().
is_blacklisted(TokenID, AuthorityID) ->
check_entry({AuthorityID, TokenID}).
%%
-spec init(options()) -> {ok, {supervisor:sup_flags(), [supervisor:child_spec()]}}.
init(Options) ->
_ = init_tab(),
_ = load_blacklist_conf(maps:get(path, Options, undefined)),
{ok, {#{}, []}}.
init_tab() ->
ets:new(?TAB, [set, protected, named_table, {read_concurrency, true}]).
-define(ENTRIES_KEY, "entries").
load_blacklist_conf(undefined) ->
_ = logger:warning("No token blacklist file specified! Token blacklisting functionality will not be enabled."),
put_entires(#{});
_ = logger:warning("No token blacklist file specified! Blacklisting functionality will be disabled."),
ok;
load_blacklist_conf(Filename) ->
[Mappings] = yamerl_constr:file(Filename),
Entries = process_entries(proplists:get_value(?ENTRIES_KEY, Mappings)),
@ -68,17 +61,23 @@ load_blacklist_conf(Filename) ->
process_entries(Entries) ->
lists:foldl(
fun({K, V}, Acc) ->
Acc#{list_to_atom(K) => [list_to_binary(V0) || V0 <- V]}
fun({AuthorityID, TokenIDs}, Acc) ->
Acc ++ [make_ets_entry(AuthorityID, ID) || ID <- TokenIDs]
end,
#{},
[],
Entries
).
make_ets_entry(AuthorityID, TokenID) ->
{{list_to_binary(AuthorityID), list_to_binary(TokenID)}, true}.
%%
put_entires(Entries) ->
persistent_term:put(?TERM_KEY, Entries).
ets:insert_new(?TAB, Entries).
get_entires() ->
persistent_term:get(?TERM_KEY).
check_entry(Key) ->
case ets:lookup(?TAB, Key) of
[_Entry] -> true;
[] -> false
end.

View File

@ -1,29 +1,20 @@
-module(tk_token_claim_utils).
-module(tk_claim_utils).
-include_lib("token_keeper_proto/include/tk_context_thrift.hrl").
-export([decode_authdata/2]).
-export([decode_authdata/1]).
-export([encode_authdata/1]).
-type decode_opts() :: #{
compatibility => {true, compatibility_opts()} | false
}.
-type compatibility_opts() :: #{
metadata_mappings := #{
party_id := binary(),
token_consumer := binary()
}
}.
-export_type([decode_opts/0]).
-export_type([compatibility_opts/0]).
-export([decode_bouncer_claim/1]).
-export([encode_bouncer_claim/1]).
%%
-type storable_authdata() :: tk_storage:storable_authdata().
-type claim() :: tk_token_jwt:claim().
-type claims() :: tk_token_jwt:claims().
-type authdata() :: tk_authdata:prototype().
-type encoded_context_fragment() :: tk_context_thrift:'ContextFragment'().
-type claim() :: term().
-type claims() :: tk_token:payload().
-define(CLAIM_BOUNCER_CTX, <<"bouncer_ctx">>).
-define(CLAIM_TK_METADATA, <<"tk_metadata">>).
@ -34,13 +25,13 @@
%%
-spec decode_authdata(claims(), decode_opts()) ->
{ok, storable_authdata()}
-spec decode_authdata(claims()) ->
{ok, authdata()}
| {error, not_found | {claim_decode_error, {unsupported, claim()} | {malformed, binary()}}}.
decode_authdata(#{?CLAIM_BOUNCER_CTX := BouncerClaim} = Claims, Opts) ->
decode_authdata(#{?CLAIM_BOUNCER_CTX := BouncerClaim} = Claims) ->
case decode_bouncer_claim(BouncerClaim) of
{ok, ContextFragment} ->
case get_metadata(Claims, Opts) of
case get_metadata(Claims) of
{ok, Metadata} ->
{ok, create_authdata(ContextFragment, Metadata)};
{error, no_metadata_claim} ->
@ -49,10 +40,10 @@ decode_authdata(#{?CLAIM_BOUNCER_CTX := BouncerClaim} = Claims, Opts) ->
{error, Reason} ->
{error, {claim_decode_error, Reason}}
end;
decode_authdata(_Claims, _Opts) ->
decode_authdata(_Claims) ->
{error, not_found}.
-spec encode_authdata(storable_authdata()) -> claims().
-spec encode_authdata(authdata()) -> claims().
encode_authdata(#{context := ContextFragment} = AuthData) ->
#{
?CLAIM_BOUNCER_CTX => encode_bouncer_claim(ContextFragment),
@ -61,6 +52,7 @@ encode_authdata(#{context := ContextFragment} = AuthData) ->
%%
-spec decode_bouncer_claim(claims()) -> {ok, encoded_context_fragment()} | {error, {malformed, binary()}}.
decode_bouncer_claim(#{
?CLAIM_CTX_TYPE := ?CLAIM_CTX_TYPE_V1_THRIFT_BINARY,
?CLAIM_CTX_CONTEXT := Content
@ -79,6 +71,7 @@ decode_bouncer_claim(#{
decode_bouncer_claim(Ctx) ->
{error, {unsupported, Ctx}}.
-spec encode_bouncer_claim(encoded_context_fragment()) -> claims().
encode_bouncer_claim(
#bctx_ContextFragment{
type = v1_thrift_binary,
@ -90,30 +83,21 @@ encode_bouncer_claim(
?CLAIM_CTX_CONTEXT => base64:encode(Content)
}.
%%
encode_metadata(#{metadata := Metadata}) ->
Metadata;
encode_metadata(#{}) ->
#{}.
get_metadata(#{?CLAIM_TK_METADATA := Metadata}, _Opts) ->
get_metadata(#{?CLAIM_TK_METADATA := Metadata}) ->
{ok, Metadata};
get_metadata(Claims, #{compatibility := {true, CompatOpts}}) ->
{ok, create_metadata(Claims, CompatOpts)};
get_metadata(_Claims, _Opts) ->
get_metadata(_Claims) ->
{error, no_metadata_claim}.
create_authdata(ContextFragment, Metadata) ->
genlib_map:compact(#{
#{
status => active,
context => ContextFragment,
metadata => Metadata
}).
create_metadata(Claims, CompatOpts) ->
Metadata = #{
%% TODO: This is a temporary hack.
%% When some external services will stop requiring woody user identity to be present it must be removed too
party_id => maps:get(<<"sub">>, Claims, undefined),
consumer => maps:get(<<"cons">>, Claims, undefined)
},
tk_utils:remap(genlib_map:compact(Metadata), maps:get(metadata_mappings, CompatOpts)).
}.

View File

@ -0,0 +1,47 @@
-module(tk_context_extractor).
%% Behaviour
-callback extract_context(token_data(), opts()) -> extracted_context() | undefined.
%% API functions
-export([extract_context/2]).
%% API Types
-type methods() :: [method_opts()].
-type method_opts() ::
{detect_token, tk_context_extractor_detect_token:opts()}
| {phony_api_key, tk_context_extractor_phony_api_key:opts()}
| {user_session_token, tk_context_extractor_user_session_token:opts()}
| {invoice_template_access_token, tk_context_extractor_invoice_tpl_token:opts()}.
-type extracted_context() :: {context_fragment(), tk_authdata:metadata() | undefined}.
-export_type([methods/0]).
-export_type([method_opts/0]).
-export_type([extracted_context/0]).
%% Internal types
-type token_data() :: tk_token:token_data().
-type context_fragment() :: bouncer_context_helpers:context_fragment().
-type opts() ::
tk_context_extractor_detect_token:opts()
| tk_context_extractor_phony_api_key:opts()
| tk_context_extractor_user_session_token:opts()
| tk_context_extractor_invoice_tpl_token:opts().
%% API functions
-spec extract_context(method_opts(), token_data()) -> extracted_context() | undefined.
extract_context({detect_token, Opts}, TokenData) ->
tk_context_extractor_detect_token:extract_context(TokenData, Opts);
extract_context({phony_api_key, Opts}, TokenData) ->
tk_context_extractor_phony_api_key:extract_context(TokenData, Opts);
extract_context({user_session_token, Opts}, TokenData) ->
tk_context_extractor_user_session_token:extract_context(TokenData, Opts);
extract_context({invoice_template_access_token, Opts}, TokenData) ->
tk_context_extractor_invoice_tpl_token:extract_context(TokenData, Opts).
%% Internal functions

View File

@ -0,0 +1,46 @@
-module(tk_context_extractor_detect_token).
-behaviour(tk_context_extractor).
%% Behaviour
-export([extract_context/2]).
%% API Types
-type opts() :: #{
phony_api_key_opts := tk_context_extractor_phony_api_key:opts(),
user_session_token_opts := tk_context_extractor_user_session_token:opts(),
user_session_token_origins := list(binary())
}.
-export_type([opts/0]).
%% Behaviour
-spec extract_context(tk_token:token_data(), opts()) -> tk_context_extractor:extracted_context() | undefined.
extract_context(VerifiedToken, Opts) ->
TokenType = determine_token_type(get_source_context(VerifiedToken), Opts),
tk_context_extractor:extract_context(make_method_opts(TokenType, Opts), VerifiedToken).
%% Internal functions
get_source_context(#{source_context := SourceContext}) ->
SourceContext.
determine_token_type(#{request_origin := Origin}, #{user_session_token_origins := UserTokenOrigins}) ->
case lists:member(Origin, UserTokenOrigins) of
true ->
user_session_token;
false ->
phony_api_key
end;
determine_token_type(#{}, _UserTokenOrigins) ->
phony_api_key.
make_method_opts(TokenType, Opts) ->
{TokenType, get_opts(TokenType, Opts)}.
get_opts(user_session_token, #{user_session_token_opts := Opts}) ->
Opts;
get_opts(phony_api_key, #{phony_api_key_opts := Opts}) ->
Opts.

View File

@ -1,42 +1,47 @@
-module(tk_extractor_invoice_tpl_token).
-module(tk_context_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_extractor).
-behaviour(tk_context_extractor).
-export([get_context/2]).
-export([extract_context/2]).
%%
-type extractor_opts() :: #{
-type opts() :: #{
domain := binary(),
metadata_mappings := #{
party_id := binary()
}
}.
-export_type([extractor_opts/0]).
-export_type([opts/0]).
%%
-define(CLAIM_PARTY_ID, <<"sub">>).
-define(CLAIM_RESOURCE_ACCESS, <<"resource_access">>).
%% API functions
-spec get_context(tk_token_jwt:t(), extractor_opts()) -> tk_extractor:extracted_context().
get_context(Token, ExtractorOpts) ->
UserID = tk_token_jwt:get_subject_id(Token),
case extract_invoice_template_rights(Token, ExtractorOpts) of
-spec extract_context(tk_token:token_data(), opts()) -> tk_context_extractor:extracted_context() | undefined.
extract_context(#{id := TokenID, payload := Payload}, Opts) ->
PartyID = maps:get(?CLAIM_PARTY_ID, Payload),
case extract_invoice_template_rights(Payload, Opts) of
{ok, InvoiceTemplateID} ->
BCtx = create_bouncer_ctx(tk_token_jwt:get_token_id(Token), UserID, InvoiceTemplateID),
BCtx = create_bouncer_ctx(TokenID, PartyID, InvoiceTemplateID),
{BCtx,
make_metadata(
#{
%% @TEMP: This is a temporary hack.
%% When some external services will stop requiring woody user
%% identity to be present it must be removed too
party_id => tk_token_jwt:get_subject_id(Token)
party_id => PartyID
},
ExtractorOpts
Opts
)};
{error, Reason} ->
_ = logger:warning("Failed to extract invoice template rights: ~p", [Reason]),
@ -45,9 +50,9 @@ get_context(Token, ExtractorOpts) ->
%%
extract_invoice_template_rights(TokenContext, ExtractorOpts) ->
Domain = maps:get(domain, ExtractorOpts),
case get_acl(Domain, get_resource_hierarchy(), TokenContext) of
extract_invoice_template_rights(TokenPayload, Opts) ->
Domain = maps:get(domain, Opts),
case get_acl(Domain, get_resource_hierarchy(), TokenPayload) of
{ok, TokenACL} ->
match_invoice_template_acl(TokenACL);
{error, Reason} ->
@ -79,27 +84,27 @@ run_pattern(Entry, Pat) when is_function(Pat, 1) ->
error:function_clause -> []
end.
get_acl(Domain, Hierarchy, TokenContext) ->
case tk_token_jwt:get_claim(<<"resource_access">>, TokenContext, undefined) of
get_acl(Domain, Hierarchy, TokenPayload) ->
case maps:get(?CLAIM_RESOURCE_ACCESS, TokenPayload, undefined) of
#{Domain := #{<<"roles">> := Roles}} ->
try
TokenACL = tk_token_legacy_acl:decode(Roles, Hierarchy),
{ok, tk_token_legacy_acl:to_list(TokenACL)}
TokenACL = tk_legacy_acl:decode(Roles, Hierarchy),
{ok, tk_legacy_acl:to_list(TokenACL)}
catch
error:Reason -> {error, {invalid, Reason}}
throw:Reason -> {error, {invalid, Reason}}
end;
_ ->
{error, missing}
end.
create_bouncer_ctx(TokenID, UserID, InvoiceTemplateID) ->
create_bouncer_ctx(TokenID, PartyID, InvoiceTemplateID) ->
bouncer_context_helpers:add_auth(
#{
method => <<"InvoiceTemplateAccessToken">>,
token => #{id => TokenID},
scope => [
#{
party => #{id => UserID},
party => #{id => PartyID},
invoice_template => #{id => InvoiceTemplateID}
}
]

View File

@ -0,0 +1,45 @@
-module(tk_context_extractor_phony_api_key).
-behaviour(tk_context_extractor).
-export([extract_context/2]).
%%
-type opts() :: #{
metadata_mappings := #{
party_id := binary()
}
}.
-export_type([opts/0]).
%%
-define(CLAIM_PARTY_ID, <<"sub">>).
%% API functions
-spec extract_context(tk_token:token_data(), opts()) -> tk_context_extractor:extracted_context() | undefined.
extract_context(#{id := TokenID, payload := Payload}, Opts) ->
PartyID = maps:get(?CLAIM_PARTY_ID, Payload),
ContextFragment = bouncer_context_helpers:add_auth(
#{
method => <<"ApiKeyToken">>,
token => #{id => TokenID},
scope => [#{party => #{id => PartyID}}]
},
bouncer_context_helpers:empty()
),
{ContextFragment,
make_metadata(
#{
party_id => PartyID
},
Opts
)}.
%%
make_metadata(Metadata, Opts) ->
Mappings = maps:get(metadata_mappings, Opts),
tk_utils:remap(genlib_map:compact(Metadata), Mappings).

View File

@ -1,11 +1,11 @@
-module(tk_extractor_user_session_token).
-behaviour(tk_extractor).
-module(tk_context_extractor_user_session_token).
-behaviour(tk_context_extractor).
-export([get_context/2]).
-export([extract_context/2]).
%%
-type extractor_opts() :: #{
-type opts() :: #{
metadata_mappings := #{
user_id := binary(),
user_email := binary(),
@ -14,16 +14,20 @@
user_realm := binary()
}.
-export_type([extractor_opts/0]).
-export_type([opts/0]).
%%
-define(CLAIM_USER_ID, <<"sub">>).
-define(CLAIM_USER_EMAIL, <<"email">>).
%% API functions
-spec get_context(tk_token_jwt:t(), extractor_opts()) -> tk_extractor:extracted_context().
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),
-spec extract_context(tk_token:token_data(), opts()) -> tk_context_extractor:extracted_context() | undefined.
extract_context(#{id := TokenID, expiration := Expiration, payload := Payload}, Opts) ->
UserID = maps:get(?CLAIM_USER_ID, Payload),
Email = maps:get(?CLAIM_USER_EMAIL, Payload),
UserRealm = maps:get(user_realm, Opts, undefined),
Acc0 = bouncer_context_helpers:empty(),
Acc1 = bouncer_context_helpers:add_user(
#{
@ -37,7 +41,7 @@ get_context(Token, ExtractorOpts) ->
#{
method => <<"SessionToken">>,
expiration => make_auth_expiration(Expiration),
token => #{id => tk_token_jwt:get_token_id(Token)}
token => #{id => TokenID}
},
Acc1
),
@ -48,14 +52,14 @@ get_context(Token, ExtractorOpts) ->
user_email => Email,
user_realm => UserRealm
},
ExtractorOpts
Opts
)}.
%% Internal functions
make_auth_expiration(Timestamp) when is_integer(Timestamp) ->
genlib_rfc3339:format(Timestamp, second);
make_auth_expiration(Expiration) when Expiration =:= unlimited; Expiration =:= undefined ->
make_auth_expiration(Expiration) when Expiration =:= unlimited ->
undefined.
make_metadata(Metadata, ExtractorOpts) ->

View File

@ -1,47 +0,0 @@
-module(tk_extractor).
%% Behaviour
-callback get_context(tk_token_jwt:t(), extractor_opts()) -> extracted_context() | undefined.
%% API functions
-export([get_context/3]).
%% API Types
-type methods() :: [{method(), extractor_opts()} | method()].
-type method() :: detect_token | api_key_token | user_session_token | invoice_template_access_token.
-type extractor_opts() ::
tk_extractor_detect_token:extractor_opts()
| tk_extractor_phony_api_key: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}.
-type context_fragment() :: bouncer_context_helpers:context_fragment().
-export_type([methods/0]).
-export_type([method/0]).
-export_type([extractor_opts/0]).
-export_type([extracted_context/0]).
-export_type([context_fragment/0]).
%% API functions
-spec get_context(method(), tk_token_jwt:t(), extractor_opts()) -> extracted_context() | undefined.
get_context(Method, Token, Opts) ->
Hander = get_extractor_handler(Method),
Hander:get_context(Token, Opts).
%%
get_extractor_handler(detect_token) ->
tk_extractor_detect_token;
get_extractor_handler(phony_api_key) ->
tk_extractor_phony_api_key;
get_extractor_handler(user_session_token) ->
tk_extractor_user_session_token;
get_extractor_handler(invoice_template_access_token) ->
tk_extractor_invoice_tpl_token.

View File

@ -1,46 +0,0 @@
-module(tk_extractor_detect_token).
-behaviour(tk_extractor).
%% Behaviour
-export([get_context/2]).
%% API Types
-type token_source() :: #{
request_origin => binary()
}.
-type extractor_opts() :: #{
phony_api_key_opts := tk_extractor_phony_api_key:extractor_opts(),
user_session_token_opts := tk_extractor_user_session_token:extractor_opts(),
user_session_token_origins := list(binary())
}.
-export_type([extractor_opts/0]).
-export_type([token_source/0]).
%% Behaviour
-spec get_context(tk_token_jwt:t(), extractor_opts()) -> tk_extractor:extracted_context() | undefined.
get_context(Token, Opts = #{user_session_token_origins := UserTokenOrigins}) ->
TokenSourceContext = tk_token_jwt:get_source_context(Token),
TokenType = determine_token_type(TokenSourceContext, UserTokenOrigins),
tk_extractor:get_context(TokenType, Token, get_opts(TokenType, Opts)).
%% Internal functions
determine_token_type(#{request_origin := Origin}, UserTokenOrigins) ->
case lists:member(Origin, UserTokenOrigins) of
true ->
user_session_token;
false ->
phony_api_key
end;
determine_token_type(#{}, _UserTokenOrigins) ->
phony_api_key.
get_opts(user_session_token, #{user_session_token_opts := Opts}) ->
Opts;
get_opts(phony_api_key, #{phony_api_key_opts := Opts}) ->
Opts.

View File

@ -1,42 +0,0 @@
-module(tk_extractor_phony_api_key).
-behaviour(tk_extractor).
-export([get_context/2]).
%%
-type extractor_opts() :: #{
metadata_mappings := #{
party_id := binary()
}
}.
-export_type([extractor_opts/0]).
%% API functions
-spec get_context(tk_token_jwt:t(), extractor_opts()) -> tk_extractor:extracted_context().
get_context(Token, ExtractorOpts) ->
PartyID = tk_token_jwt:get_subject_id(Token),
Acc0 = bouncer_context_helpers:empty(),
Acc1 = bouncer_context_helpers:add_auth(
#{
method => <<"ApiKeyToken">>,
token => #{id => tk_token_jwt:get_token_id(Token)},
scope => [#{party => #{id => PartyID}}]
},
Acc0
),
{Acc1,
make_metadata(
#{
party_id => PartyID
},
ExtractorOpts
)}.
%%
make_metadata(Metadata, ExtractorOpts) ->
Mappings = maps:get(metadata_mappings, ExtractorOpts),
tk_utils:remap(genlib_map:compact(Metadata), Mappings).

153
src/tk_handler.erl Normal file
View File

@ -0,0 +1,153 @@
-module(tk_handler).
-callback handle_function(woody:func(), woody:args(), handler_opts(), state()) -> {ok, woody:result()} | no_return().
%% Config API
-export([get_authenticator_handler/2]).
-export([get_authority_handler/3]).
%% Woody handler
-behaviour(woody_server_thrift_handler).
-export([handle_function/4]).
-type ctx() :: #{
woody_context := woody_context:ctx()
}.
-type handler_opts() ::
tk_handler_authenticator:opts()
| tk_handler_authority_ephemeral:opts()
| tk_handler_authority_offline:opts().
-type opts() :: #{
handler := {module(), handler_opts()},
default_handling_timeout => timeout(),
pulse => tk_pulse:handlers()
}.
-type state() :: #{
context := ctx(),
pulse := tk_pulse:handlers(),
pulse_metadata := tk_pulse:metadata()
}.
-export_type([ctx/0]).
-export_type([opts/0]).
-export_type([state/0]).
%% Config types
-type service_handler_configuration() :: #{
path => binary()
}.
-type authenticator_opts() :: #{
service => service_handler_configuration(),
authorities => authenticator_authoritites()
}.
-type authenticator_authoritites() :: #{authority_id() => authenticator_authority()}.
-type authenticator_authority() :: #{
sources => [tk_authdata_source:authdata_source()]
}.
-type authority_opts() :: #{
service => service_handler_configuration(),
type => authority_type()
}.
-type authority_type() :: ephemeral_authority_type() | offline_authority_type().
-type ephemeral_authority_type() ::
{ephemeral, #{
token => authority_token_config()
}}.
-type offline_authority_type() ::
{offline, #{
token => authority_token_config(),
storage => authority_storage_config()
}}.
-type authority_token_config() :: #{
type => tk_token:token_type()
}.
-type authority_storage_config() :: #{
name => tk_storage:storage_name()
}.
-export_type([authenticator_opts/0]).
-export_type([authority_opts/0]).
%%
-type authority_id() :: tk_authdata:authority_id().
%%
-define(DEFAULT_HANDLING_TIMEOUT, 30000).
%%
-spec get_authenticator_handler(authenticator_opts(), tk_pulse:handlers()) -> woody:http_handler(woody:th_handler()).
get_authenticator_handler(Opts, AuditPulse) ->
get_http_handler(
maps:get(service, Opts),
get_authenticator_handler_spec(Opts),
AuditPulse
).
-spec get_authority_handler(authority_id(), authority_opts(), tk_pulse:handlers()) ->
woody:http_handler(woody:th_handler()).
get_authority_handler(AuthorityID, Opts, AuditPulse) ->
get_http_handler(
maps:get(service, Opts),
get_authority_handler_spec(AuthorityID, maps:get(type, Opts)),
AuditPulse
).
%%
-spec handle_function(woody:func(), woody:args(), woody_context:ctx(), opts()) -> {ok, woody:result()} | no_return().
handle_function(Op, Args, WoodyContext0, #{handler := {Handler, HandlerOpts}} = Opts) ->
WoodyContext = ensure_woody_deadline_set(WoodyContext0, Opts),
Handler:handle_function(Op, Args, HandlerOpts, make_state(WoodyContext, Opts)).
%%
make_state(WoodyCtx, Opts) ->
#{
context => make_context(WoodyCtx),
pulse => maps:get(pulse, Opts, []),
pulse_metadata => #{woody_ctx => WoodyCtx}
}.
make_context(WoodyCtx) ->
#{woody_context => WoodyCtx}.
ensure_woody_deadline_set(WoodyContext, Opts) ->
case woody_context:get_deadline(WoodyContext) of
undefined ->
DefaultTimeout = maps:get(default_handling_timeout, Opts, ?DEFAULT_HANDLING_TIMEOUT),
Deadline = woody_deadline:from_timeout(DefaultTimeout),
woody_context:set_deadline(Deadline, WoodyContext);
_Other ->
WoodyContext
end.
get_http_handler(ServiceConf, HandlerSpec, AuditPulse) ->
{maps:get(path, ServiceConf), wrap_handler_spec(HandlerSpec, AuditPulse)}.
wrap_handler_spec({ServiceName, Handler}, AuditPulse) ->
{ServiceName, {tk_handler, #{handler => Handler, pulse => AuditPulse}}}.
get_authority_handler_spec(AuthorityID, {ephemeral, Opts}) ->
tk_handler_authority_ephemeral:get_handler_spec(AuthorityID, Opts);
get_authority_handler_spec(AuthorityID, {offline, Opts}) ->
tk_handler_authority_offline:get_handler_spec(AuthorityID, Opts).
get_authenticator_handler_spec(Opts) ->
tk_handler_authenticator:get_handler_spec(maps:with([authorities], Opts)).

View File

@ -0,0 +1,144 @@
-module(tk_handler_authenticator).
-include_lib("token_keeper_proto/include/tk_context_thrift.hrl").
-include_lib("token_keeper_proto/include/tk_token_keeper_thrift.hrl").
-export([get_handler_spec/1]).
%% Woody handler
-behaviour(tk_handler).
-export([handle_function/4]).
-type handler_config() :: #{
authorities := authorities()
}.
-type opts() :: handler_config().
-export_type([handler_config/0]).
-export_type([opts/0]).
%% Internal types
-type authority_id() :: tk_authdata:authority_id().
-type authorities() :: #{authority_id() => authority_opts()}.
-type authority_opts() :: #{sources := [tk_authdata_source:authdata_source()]}.
%%
-spec get_handler_spec(handler_config()) -> woody:th_handler().
get_handler_spec(Opts) ->
{
{tk_token_keeper_thrift, 'TokenAuthenticator'},
{?MODULE, Opts}
}.
%%
-spec handle_function(woody:func(), woody:args(), opts(), tk_handler:state()) ->
{ok, woody:result()} | no_return().
handle_function('AddExistingToken', _Args, _Opts, _State) ->
erlang:error(not_implemented);
handle_function('Authenticate' = Op, {Token, TokenSourceContext}, Opts, State) ->
_ = pulse_op_stated(Op, State),
case tk_token:verify(Token, decode_source_context(TokenSourceContext)) of
{ok, TokenData} ->
State1 = save_pulse_metadata(#{token => TokenData}, State),
case get_authdata(TokenData, Opts, State) of
{ok, #{status := Status} = AuthDataPrototype} when Status =/= revoked ->
EncodedAuthData = encode_auth_data(AuthDataPrototype#{token => Token}),
_ = pulse_op_succeeded(Op, State1),
{ok, EncodedAuthData};
{ok, #{status := _}} ->
_ = pulse_op_failed(Op, authdata_revoked, State),
woody_error:raise(business, #token_keeper_AuthDataRevoked{});
{error, Reason} ->
_ = pulse_op_failed(Op, Reason, State),
woody_error:raise(business, #token_keeper_AuthDataNotFound{})
end;
{error, {verification_failed, _} = Reason} ->
_ = pulse_op_failed(Op, Reason, State),
woody_error:raise(business, #token_keeper_InvalidToken{});
{error, blacklisted = Reason} ->
_ = pulse_op_failed(Op, Reason, State),
woody_error:raise(business, #token_keeper_AuthDataRevoked{})
end.
%% Internal functions
get_authdata(TokenData = #{authority_id := AuthorityID}, Opts, #{context := Context}) ->
case get_authdata_by_authority(get_authority_config(AuthorityID, Opts), TokenData, Context) of
{ok, AuthData} ->
{ok, maybe_add_authority_id(AuthData, AuthorityID)};
{error, _} = Error ->
Error
end.
get_authdata_by_authority(#{sources := Sources}, TokenData, #{woody_context := WoodyCtx}) ->
get_authdata_from_sources(Sources, TokenData, WoodyCtx).
get_authdata_from_sources([], _TokenData, _WoodyCtx) ->
{error, not_found};
get_authdata_from_sources([SourceOpts | Rest], TokenData, WoodyCtx) ->
case tk_authdata_source:get_authdata(TokenData, SourceOpts, WoodyCtx) of
undefined ->
%% @TODO: Gather errors process them here, instead of relying on logger:warnings at source level
get_authdata_from_sources(Rest, TokenData, WoodyCtx);
AuthData ->
{ok, AuthData}
end.
get_authority_config(AuthorityID, #{authorities := Configs}) ->
maps:get(AuthorityID, Configs).
maybe_add_authority_id(AuthData = #{authority := _}, _AuthorityID) ->
AuthData;
maybe_add_authority_id(AuthData, AuthorityID) ->
AuthData#{authority => AuthorityID}.
%%
decode_source_context(#token_keeper_TokenSourceContext{
request_origin = RequestOrigin
}) ->
genlib_map:compact(#{
request_origin => RequestOrigin
}).
encode_auth_data(
#{
token := Token,
status := Status,
context := Context,
authority := Authority
} = AuthData
) ->
#token_keeper_AuthData{
id = maps:get(id, AuthData, undefined),
token = Token,
status = Status,
context = Context,
metadata = maps:get(metadata, AuthData, #{}),
authority = Authority
}.
%%
save_pulse_metadata(Metadata, State = #{pulse_metadata := PulseMetadata}) ->
State#{pulse_metadata => maps:merge(Metadata, PulseMetadata)}.
pulse_op_stated(Op, State) ->
handle_beat(Op, started, State).
pulse_op_succeeded(Op, State) ->
handle_beat(Op, succeeded, State).
pulse_op_failed(Op, Reason, State) ->
handle_beat(Op, {failed, Reason}, State).
encode_beat_op('Authenticate') ->
{authenticator, authenticate}.
handle_beat(Op, Event, #{pulse_metadata := PulseMetadata, pulse := Pulse}) ->
tk_pulse:handle_beat({encode_beat_op(Op), Event}, PulseMetadata, Pulse).

View File

@ -0,0 +1,106 @@
-module(tk_handler_authority_ephemeral).
-include_lib("token_keeper_proto/include/tk_context_thrift.hrl").
-include_lib("token_keeper_proto/include/tk_token_keeper_thrift.hrl").
-export([get_handler_spec/2]).
%% Woody handler
-behaviour(tk_handler).
-export([handle_function/4]).
-type handler_config() :: #{
token := token_opts()
}.
-type opts() :: #{
authority_id := tk_token:authority_id(),
token_type := tk_token:token_type()
}.
-export_type([handler_config/0]).
-export_type([opts/0]).
%% Internal types
-type token_opts() :: #{
type := tk_token:token_type()
}.
-type authority_id() :: tk_authdata:authority_id().
%%
-spec get_handler_spec(authority_id(), handler_config()) -> woody:th_handler().
get_handler_spec(AuthorityID, Config) ->
Token = maps:get(token, Config),
{
{tk_token_keeper_thrift, 'EphemeralTokenAuthority'},
{?MODULE, #{
authority_id => AuthorityID,
token_type => maps:get(type, Token)
}}
}.
%%
-spec handle_function(woody:func(), woody:args(), opts(), tk_handler:state()) -> {ok, woody:result()} | no_return().
handle_function('Create' = Op, {ContextFragment, Metadata}, Opts, State) ->
_ = pulse_op_stated(Op, State),
AuthDataPrototype = create_auth_data(ContextFragment, Metadata),
Claims = tk_claim_utils:encode_authdata(AuthDataPrototype),
{ok, Token} = tk_token_jwt:issue(create_token_data(Claims, Opts)),
EncodedAuthData = encode_auth_data(AuthDataPrototype#{token => Token}),
_ = pulse_op_succeeded(Op, State),
{ok, EncodedAuthData}.
%% Internal functions
create_auth_data(ContextFragment, Metadata) ->
tk_authdata:create_prototype(undefined, ContextFragment, Metadata).
%%
create_token_data(Claims, #{authority_id := AuthorityID, token_type := TokenType}) ->
#{
id => unique_id(),
type => TokenType,
authority_id => AuthorityID,
expiration => unlimited,
payload => Claims
}.
unique_id() ->
<<ID:64>> = snowflake:new(),
genlib_format:format_int_base(ID, 62).
%%
encode_auth_data(
#{
token := Token,
status := Status,
context := Context
} = AuthData
) ->
#token_keeper_AuthData{
token = Token,
status = Status,
context = Context,
metadata = maps:get(metadata, AuthData, #{})
}.
%%
pulse_op_stated(Op, State) ->
handle_beat(Op, started, State).
pulse_op_succeeded(Op, State) ->
handle_beat(Op, succeeded, State).
encode_beat_op('Create') ->
{authority, {ephemeral, create}}.
handle_beat(Op, Event, #{pulse_metadata := PulseMetadata, pulse := Pulse}) ->
tk_pulse:handle_beat({encode_beat_op(Op), Event}, PulseMetadata, Pulse).

View File

@ -0,0 +1,168 @@
-module(tk_handler_authority_offline).
-include_lib("token_keeper_proto/include/tk_context_thrift.hrl").
-include_lib("token_keeper_proto/include/tk_token_keeper_thrift.hrl").
-export([get_handler_spec/2]).
%% Woody handler
-behaviour(tk_handler).
-export([handle_function/4]).
-type handler_config() :: #{
token := token_opts(),
storage := storage_opts()
}.
-type opts() :: #{
authority_id := tk_token:authority_id(),
token_type := tk_token:token_type(),
storage_name := tk_storage:storage_name()
}.
-export_type([handler_config/0]).
-export_type([opts/0]).
%% Internal types
-type storage_opts() :: #{
name := tk_storage:storage_name()
}.
-type token_opts() :: #{
type := tk_token:token_type()
}.
-type authority_id() :: tk_authdata:authority_id().
%%
-spec get_handler_spec(authority_id(), handler_config()) -> woody:th_handler().
get_handler_spec(AuthorityID, Config) ->
Token = maps:get(token, Config),
Storage = maps:get(storage, Config),
{
{tk_token_keeper_thrift, 'TokenAuthority'},
{?MODULE, #{
authority_id => AuthorityID,
token_type => maps:get(type, Token),
storage_name => maps:get(name, Storage)
}}
}.
%%
-spec handle_function(woody:func(), woody:args(), opts(), tk_handler:state()) -> {ok, woody:result()} | no_return().
handle_function('Create' = Op, {ID, ContextFragment, Metadata}, Opts, State) ->
%% Create - создает новую AuthData, используя переданные в качестве
%% аргументов данные и сохраняет их в хранилище, после чего выписывает
%% новый JWT-токен, в котором содержится AuthDataID (на данный момент
%% предполагается, что AuthDataID == jwt-клейму JTI). По умолчанию
%% status токена - active; authority - id выписывающей authority.
_ = pulse_op_stated(Op, State),
State1 = save_pulse_metadata(#{authdata_id => ID}, State),
AuthData = create_auth_data(ID, ContextFragment, Metadata),
case store(AuthData, Opts, get_context(State1)) of
ok ->
{ok, Token} = tk_token_jwt:issue(create_token_data(ID, Opts)),
EncodedAuthData = encode_auth_data(AuthData#{token => Token}),
_ = pulse_op_succeeded(Op, State1),
{ok, EncodedAuthData};
{error, exists} ->
_ = pulse_op_failed(Op, exists, State1),
woody_error:raise(business, #token_keeper_AuthDataAlreadyExists{})
end;
handle_function('Get' = Op, {ID}, Opts, State) ->
_ = pulse_op_stated(Op, State),
State1 = save_pulse_metadata(#{authdata_id => ID}, State),
case get_authdata(ID, Opts, get_context(State1)) of
{ok, AuthDataPrototype} ->
%% The initial token is not recoverable at this point
EncodedAuthData = encode_auth_data(AuthDataPrototype),
_ = pulse_op_succeeded(Op, State1),
{ok, EncodedAuthData};
{error, Reason} ->
_ = pulse_op_failed(Op, Reason, State1),
woody_error:raise(business, #token_keeper_AuthDataNotFound{})
end;
handle_function('Revoke' = Op, {ID}, Opts, State) ->
_ = pulse_op_stated(Op, State),
State1 = save_pulse_metadata(#{authdata_id => ID}, State),
case revoke(ID, Opts, get_context(State1)) of
ok ->
_ = pulse_op_succeeded(Op, State1),
{ok, ok};
{error, notfound = Reason} ->
_ = pulse_op_failed(Op, Reason, State1),
woody_error:raise(business, #token_keeper_AuthDataNotFound{})
end.
%% Internal functions
create_auth_data(ID, ContextFragment, Metadata) ->
tk_authdata:create_prototype(ID, ContextFragment, Metadata).
create_token_data(ID, #{authority_id := AuthorityID, token_type := TokenType}) ->
#{
id => ID,
type => TokenType,
authority_id => AuthorityID,
expiration => unlimited,
payload => #{}
}.
%%
get_authdata(ID, #{storage_name := StorageName}, #{woody_context := WoodyContext}) ->
tk_storage:get(ID, StorageName, WoodyContext).
store(AuthData, #{storage_name := StorageName}, #{woody_context := WoodyContext}) ->
tk_storage:store(AuthData, StorageName, WoodyContext).
revoke(ID, #{storage_name := StorageName}, #{woody_context := WoodyContext}) ->
tk_storage:revoke(ID, StorageName, WoodyContext).
%%
get_context(#{context := Context}) ->
Context.
encode_auth_data(
#{
id := ID,
status := Status,
context := Context
} = AuthData
) ->
#token_keeper_AuthData{
id = ID,
token = maps:get(token, AuthData, undefined),
status = Status,
context = Context,
metadata = maps:get(metadata, AuthData, #{})
}.
%%
save_pulse_metadata(Metadata, State = #{pulse_metadata := PulseMetadata}) ->
State#{pulse_metadata => maps:merge(Metadata, PulseMetadata)}.
pulse_op_stated(Op, State) ->
handle_beat(Op, started, State).
pulse_op_succeeded(Op, State) ->
handle_beat(Op, succeeded, State).
pulse_op_failed(Op, Reason, State) ->
handle_beat(Op, {failed, Reason}, State).
encode_beat_op('Create') ->
{authority, {offline, create}};
encode_beat_op('Get') ->
{authority, {offline, get}};
encode_beat_op('Revoke') ->
{authority, {offline, revoke}}.
handle_beat(Op, Event, #{pulse_metadata := PulseMetadata, pulse := Pulse}) ->
tk_pulse:handle_beat({encode_beat_op(Op), Event}, PulseMetadata, Pulse).

View File

@ -1,4 +1,4 @@
-module(tk_token_legacy_acl).
-module(tk_legacy_acl).
%%
@ -48,7 +48,7 @@ decode_entry(V, ACL, ResourceHierarchy) ->
Permission = decode_permission(V2),
insert_scope(Scope, Permission, ACL, ResourceHierarchy);
_ ->
error({badarg, {role, V}})
throw({badarg, {role, V}})
end.
decode_scope(V, ResourceHierarchy) ->
@ -79,7 +79,7 @@ decode_resource(V) ->
binary_to_existing_atom(V, utf8)
catch
error:badarg ->
error({badarg, {resource, V}})
throw({badarg, {resource, V}})
end.
decode_permission(<<"read">>) ->
@ -87,7 +87,7 @@ decode_permission(<<"read">>) ->
decode_permission(<<"write">>) ->
write;
decode_permission(V) ->
error({badarg, {permission, V}}).
throw({badarg, {permission, V}}).
%%
@ -121,7 +121,7 @@ compute_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}}).
throw({badarg, {scope, Scope}}).
compute_scope_priority([{Resource, _ID} | Rest], H, P) ->
compute_scope_priority(Rest, delve(Resource, H), P * 10 + 2);
@ -137,5 +137,5 @@ delve(Resource, Hierarchy) ->
{ok, Sub} ->
Sub;
error ->
error({badarg, {resource, Resource}})
throw({badarg, {resource, Resource}})
end.

View File

@ -1,16 +1,17 @@
-module(tk_pulse).
-type beat() ::
{get_by_token,
{
beat_op(),
started
| succeeded
| {failed, _Reason}}
| {create_ephemeral,
started
| succeeded}.
| {failed, _Reason}
}.
-type metadata() :: #{
token => tk_token_jwt:t(),
authdata_id => tk_authdata:id(),
authority_id => tk_authdata:authority_id(),
token => tk_token:token_data(),
woody_ctx => woody_context:ctx()
}.
@ -19,6 +20,18 @@
%%
-type beat_op() ::
{authenticator, authenticator_op()}
| {authority, authority_op()}.
-type authenticator_op() :: authenticate.
-type authority_op() ::
{ephemeral, ephemeral_op()}
| {offline, offline_op()}.
-type ephemeral_op() :: create.
-type offline_op() :: create | get | revoke.
-type handler() :: {module(), _Opts}.
-type handler(St) :: {module(), St}.
-type handlers() :: [handler()].

View File

@ -1,72 +1,90 @@
-module(tk_storage).
-export([get/2]).
-export([store/2]).
-export([revoke/2]).
%%
-export([child_specs/1]).
-behaviour(supervisor).
-export([init/1]).
%%
-callback get(authdata_id(), storage_opts(), tk_woody_handler:handle_ctx()) ->
{ok, tk_storage:storable_authdata()} | {error, _Reason}.
-callback store(tk_storage:storable_authdata(), storage_opts(), tk_woody_handler:handle_ctx()) -> ok | {error, _Reason}.
-callback revoke(authdata_id(), storage_opts(), tk_woody_handler:handle_ctx()) -> ok | {error, _Reason}.
-export([get/3]).
-export([store/3]).
-export([revoke/3]).
%%
-type storable_authdata() :: #{
id => tk_authority:authdata_id(),
status := tk_authority:status(),
context := tk_authority:encoded_context_fragment(),
authority => tk_authority:autority_id(),
metadata => tk_authority:metadata()
}.
-export_type([storable_authdata/0]).
-callback get(authdata_id(), storage_opts(), woody_context:ctx()) -> {ok, authdata()} | {error, _Reason}.
-callback store(authdata(), storage_opts(), woody_context:ctx()) -> ok | {error, _Reason}.
-callback revoke(authdata_id(), storage_opts(), woody_context:ctx()) -> ok | {error, _Reason}.
%%
-type authdata_id() :: tk_authority:authdata_id().
-type storage_name() :: binary().
-type storage() :: machinegun.
-export_type([storage_name/0]).
%%
-type authdata() :: tk_authdata:prototype().
-type authdata_id() :: tk_authdata:id().
-type storage_opts() :: tk_storage_machinegun:storage_opts().
-type storage_config() :: storage() | {storage(), storage_opts()}.
-type storages_config() :: #{storage_name() => storage_config()}.
-type storage_config() :: machinegun_storage_config().
-type machinegun_storage_config() :: {machinegun, tk_storage_machinegun:storage_opts()}.
%%
-spec get(authdata_id(), tk_woody_handler:handle_ctx()) -> {ok, storable_authdata()} | {error, _Reason}.
get(DataID, Ctx) ->
call(DataID, get_storage_config(), Ctx, get).
-spec store(storable_authdata(), tk_woody_handler:handle_ctx()) -> ok | {error, exists}.
store(AuthData, Ctx) ->
call(AuthData, get_storage_config(), Ctx, store).
-spec revoke(authdata_id(), tk_woody_handler:handle_ctx()) -> ok | {error, notfound}.
revoke(DataID, Ctx) ->
call(DataID, get_storage_config(), Ctx, revoke).
-define(PTERM_KEY(Key), {?MODULE, Key}).
-define(STORAGE_NAME(StorageName), ?PTERM_KEY({storage_name, StorageName})).
%%
-spec get_storage_config() -> storage_config() | no_return().
get_storage_config() ->
case genlib_app:env(token_keeper, storage) of
StorageConf when StorageConf =/= undefined ->
StorageConf;
_ ->
error({misconfiguration, {storage, not_configured}})
end.
-spec child_specs(storages_config()) -> [supervisor:child_spec()].
child_specs(StorageOpts) ->
[
#{
id => ?MODULE,
start => {supervisor, start_link, [?MODULE, StorageOpts]},
type => supervisor
}
].
-spec init(storages_config()) -> {ok, {supervisor:sup_flags(), [supervisor:child_spec()]}}.
init(StoragesConfig) ->
_ = store_configs(StoragesConfig),
{ok, {#{}, []}}.
%%
-spec get(authdata_id(), storage_name(), woody_context:ctx()) -> {ok, authdata()} | {error, _Reason}.
get(AuthDataID, StorageName, Ctx) ->
call(get, AuthDataID, get_config(StorageName), Ctx).
-spec store(authdata(), storage_name(), woody_context:ctx()) -> ok | {error, exists}.
store(AuthData, StorageName, Ctx) ->
call(store, AuthData, get_config(StorageName), Ctx).
-spec revoke(authdata_id(), storage_name(), woody_context:ctx()) -> ok | {error, notfound}.
revoke(AuthDataID, StorageName, Ctx) ->
call(revoke, AuthDataID, get_config(StorageName), Ctx).
%%
store_configs(StoragesConfig) ->
maps:foreach(fun store_config/2, StoragesConfig).
store_config(StorageName, StorageOpts) ->
ok = persistent_term:put(?STORAGE_NAME(StorageName), StorageOpts).
get_config(StorageName) ->
persistent_term:get(?STORAGE_NAME(StorageName), undefined).
%%
-spec get_storage_handler(storage()) -> machinery:logic_handler(_).
get_storage_handler(machinegun) ->
tk_storage_machinegun.
call(Operand, StorageOpts, Ctx, Func) ->
{Storage, Opts} = get_storage_opts(StorageOpts),
call(Func, Operand, {Storage, Opts}, Ctx) ->
Handler = get_storage_handler(Storage),
Handler:Func(Operand, Opts, Ctx).
get_storage_opts(Storage) when is_atom(Storage) ->
{Storage, #{}};
get_storage_opts({_, _} = StorageOpts) ->
StorageOpts.

View File

@ -2,71 +2,83 @@
-include_lib("token_keeper_proto/include/tk_events_thrift.hrl").
-behaviour(tk_storage).
-behaviour(machinery).
%% API
-export([get_routes/2]).
%% tk_storage interface
-behaviour(tk_storage).
-export([get/3]).
-export([store/3]).
-export([revoke/3]).
%% API
-export([get_routes/1]).
%% machinery interface
-behaviour(machinery).
-export([init/4]).
-export([process_repair/4]).
-export([process_timeout/3]).
-export([process_call/4]).
-type storage_opts() :: #{}.
-export_type([storage_opts/0]).
-type storage_opts() :: #{
namespace := namespace(),
automaton := automaton()
}.
-define(NS, tk_authdata).
-type processor_opts() :: #{
path := binary()
}.
-export_type([storage_opts/0]).
-export_type([processor_opts/0]).
%%
-type storable_authdata() :: tk_storage:storable_authdata().
-type authdata_id() :: tk_authority:authdata_id().
-type authdata() :: tk_authdata:prototype().
-type authdata_id() :: tk_authdata:id().
-type schema() :: machinery_mg_schema_generic.
-type event_handler() :: woody:ev_handler() | [woody:ev_handler()].
-type namespace() :: atom().
-type automaton() :: #{
% machinegun's automaton url
url := binary(),
path := binary(),
event_handler := event_handler(),
schema => schema(),
transport_opts => woody_client_thrift_http_transport:transport_options()
}.
-type events() :: tk_events_thrift:'AuthDataChange'().
-type machine() :: machinery:machine(events(), any()).
-type result() :: machinery:result(events(), any()).
-type event() :: tk_storage_machinegun_schema:event().
-type machine() :: machinery:machine(event(), any()).
-type result() :: machinery:result(event(), any()).
-type handler_args() :: machinery:handler_args(any()).
-type handler_opts() :: machinery:handler_args(any()).
%%
-define(MACHINERY_SCHEMA, tk_storage_machinegun_schema).
%%-------------------------------------
%% API
-spec get_routes(processor_opts(), machinery_utils:route_opts()) -> machinery_utils:woody_routes().
get_routes(ProcessorOpts, RouteOpts) ->
machinery_mg_backend:get_routes([create_handler(ProcessorOpts)], RouteOpts).
%%-------------------------------------
%% tk_storage behaviour implementation
-spec get(authdata_id(), storage_opts(), tk_woody_handler:handle_ctx()) -> {ok, storable_authdata()} | {error, _Reason}.
get(ID, _Opts, Ctx) ->
case machinery:get(?NS, ID, backend(Ctx)) of
{ok, Machine} ->
{ok, collapse(Machine)};
-spec get(authdata_id(), storage_opts(), woody_context:ctx()) -> {ok, authdata()} | {error, _Reason}.
get(ID, #{namespace := Namespace} = Opts, Ctx) ->
case machinery:get(Namespace, ID, backend(Opts, Ctx)) of
{ok, #{history := History}} ->
{ok, collapse_history(History)};
{error, _} = Err ->
Err
end.
-spec store(storable_authdata(), storage_opts(), tk_woody_handler:handle_ctx()) -> ok | {error, exists}.
store(AuthData, _Opts, Ctx) ->
DataID = tk_authority:get_authdata_id(AuthData),
machinery:start(?NS, DataID, {store, AuthData}, backend(Ctx)).
-spec store(authdata(), storage_opts(), woody_context:ctx()) -> ok | {error, exists}.
store(#{id := AuthDataID} = AuthData, #{namespace := Namespace} = Opts, Ctx) ->
machinery:start(Namespace, AuthDataID, AuthData, backend(Opts, Ctx)).
-spec revoke(authdata_id(), storage_opts(), tk_woody_handler:handle_ctx()) -> ok | {error, notfound}.
revoke(ID, _Opts, Ctx) ->
case machinery:call(?NS, ID, revoke, backend(Ctx)) of
-spec revoke(authdata_id(), storage_opts(), woody_context:ctx()) -> ok | {error, notfound}.
revoke(ID, #{namespace := Namespace} = Opts, Ctx) ->
case machinery:call(Namespace, ID, revoke, backend(Opts, Ctx)) of
{ok, _Reply} ->
ok;
{error, notfound} = Err ->
@ -76,18 +88,10 @@ revoke(ID, _Opts, Ctx) ->
%%-------------------------------------
%% machinery behaviour implementation
-spec init(machinery:args({store, storable_authdata()}), machine(), handler_args(), handler_opts()) -> result().
init({store, AuthData}, _Machine, _, _) ->
#{
events => [
{created, #tk_events_AuthDataCreated{
id = maps:get(id, AuthData),
status = maps:get(status, AuthData),
context = maps:get(context, AuthData),
metadata = maps:get(metadata, AuthData)
}}
]
}.
-spec init(machinery:args(authdata()), machine(), handler_args(), handler_opts()) -> result().
init(AuthData, _Machine, _, _) ->
Events = create_authdata(AuthData),
#{events => Events}.
-spec process_repair(machinery:args(_), machine(), handler_args(), handler_opts()) -> no_return().
process_repair(_Args, _Machine, _, _) ->
@ -99,48 +103,56 @@ process_timeout(_Machine, _, _) ->
-spec process_call(machinery:args(revoke), machine(), handler_args(), handler_opts()) ->
{machinery:response(ok), result()}.
process_call(revoke, Machine, _, _) ->
Events = change_status(revoked, Machine),
process_call(revoke, #{history := History}, _, _) ->
AuthData = collapse_history(History),
Events = change_status(revoked, AuthData),
{ok, #{events => Events}}.
%%-------------------------------------
%% API
-spec get_routes(machinery_utils:route_opts()) -> machinery_utils:woody_routes().
get_routes(RouteOpts) ->
machinery_mg_backend:get_routes([create_handler()], RouteOpts).
%%-------------------------------------
%% internal
create_handler() ->
create_authdata(AuthData) ->
[
{created, #tk_events_AuthDataCreated{
id = maps:get(id, AuthData),
status = maps:get(status, AuthData),
context = maps:get(context, AuthData),
metadata = maps:get(metadata, AuthData)
}}
].
change_status(NewStatus, #{status := NewStatus}) ->
[];
change_status(NewStatus, #{status := _OtherStatus}) ->
[{status_changed, #tk_events_AuthDataStatusChanged{status = NewStatus}}].
%%
create_handler(ProcessorOpts) ->
{?MODULE, #{
path => <<"/v1/stateproc/storage">>,
path => maps:get(path, ProcessorOpts),
backend_config => #{
schema => machinery_mg_schema_generic
schema => ?MACHINERY_SCHEMA
}
}}.
backend(#{woody_ctx := WC}) ->
case genlib_app:env(token_keeper, service_clients, #{}) of
#{automaton := Automaton} ->
machinery_mg_backend:new(WC, #{
client => get_woody_client(Automaton),
schema => machinery_mg_schema_generic
});
#{} ->
erlang:error({misconfiguration, automaton})
end.
backend(#{automaton := Automaton}, WoodyContext) ->
machinery_mg_backend:new(WoodyContext, #{
client => get_woody_client(Automaton),
schema => ?MACHINERY_SCHEMA
}).
-spec get_woody_client(automaton()) -> machinery_mg_client:woody_client().
get_woody_client(#{url := Url} = Automaton) ->
genlib_map:compact(#{
url => Url,
event_handler => genlib_app:env(token_keeper, woody_event_handlers, [scoper_woody_event_handler]),
event_handler => maps:get(event_handler, Automaton, [scoper_woody_event_handler]),
transport_opts => maps:get(transport_opts, Automaton, undefined)
}).
collapse(#{history := History}) ->
%%
collapse_history(History) ->
collapse_history(History, undefined).
collapse_history([], AuthData) when AuthData =/= undefined ->
@ -151,11 +163,3 @@ collapse_history([{_, _, {created, AuthData}} | Rest], undefined) ->
collapse_history([{_, _, {status_changed, StatusChanged}} | Rest], AuthData) when AuthData =/= undefined ->
#tk_events_AuthDataStatusChanged{status = Status} = StatusChanged,
collapse_history(Rest, AuthData#{status => Status}).
change_status(NewStatus, Machine) ->
case collapse(Machine) of
#{status := NewStatus} ->
[];
#{status := _OtherStatus} ->
[{status_changed, #tk_events_AuthDataStatusChanged{status = NewStatus}}]
end.

View File

@ -0,0 +1,143 @@
-module(tk_storage_machinegun_schema).
%% machinery_mg_schema behaviour
-behaviour(machinery_mg_schema).
-export([get_version/1]).
-export([marshal/3]).
-export([unmarshal/3]).
%% API Types
-type event() :: tk_events_thrift:'AuthDataChange'().
-export_type([event/0]).
%% Internal types
-type type() :: machinery_mg_schema:t().
-type value(T) :: machinery_mg_schema:v(T).
-type value_type() :: machinery_mg_schema:vt().
-type context() :: machinery_mg_schema:context().
-type aux_state() :: term().
-type call_args() :: term().
-type call_response() :: term().
-type data() ::
aux_state()
| event()
| call_args()
| call_response().
%%
-define(CURRENT_EVENT_FORMAT_VERSION, 1).
%% machinery_mg_schema callbacks
-spec get_version(value_type()) -> machinery_mg_schema:version().
get_version(event) ->
?CURRENT_EVENT_FORMAT_VERSION;
get_version(aux_state) ->
undefined.
-spec marshal(type(), value(data()), context()) -> {machinery_msgpack:t(), context()}.
marshal({event, FormatVersion}, TimestampedChange, Context) ->
marshal_event(FormatVersion, TimestampedChange, Context);
marshal(T, V, C) when
T =:= {args, init} orelse
T =:= {args, call} orelse
T =:= {args, repair} orelse
T =:= {aux_state, undefined} orelse
T =:= {response, call} orelse
T =:= {response, {repair, success}} orelse
T =:= {response, {repair, failure}}
->
machinery_mg_schema_generic:marshal(T, V, C).
-spec unmarshal(type(), machinery_msgpack:t(), context()) -> {data(), context()}.
unmarshal({event, FormatVersion}, EncodedChange, Context) ->
unmarshal_event(FormatVersion, EncodedChange, Context);
unmarshal(T, V, C) when
T =:= {args, init} orelse
T =:= {args, call} orelse
T =:= {args, repair} orelse
T =:= {aux_state, undefined} orelse
T =:= {response, call} orelse
T =:= {response, {repair, success}} orelse
T =:= {response, {repair, failure}}
->
machinery_mg_schema_generic:unmarshal(T, V, C).
%% Internals
-spec marshal_event(machinery_mg_schema:version(), event(), context()) -> {machinery_msgpack:t(), context()}.
marshal_event(1, AuthDataChange, Context) ->
Type = {struct, union, {tk_events_thrift, 'AuthDataChange'}},
{{bin, serialize(Type, AuthDataChange)}, Context}.
-spec unmarshal_event(machinery_mg_schema:version(), machinery_msgpack:t(), context()) -> {event(), context()}.
unmarshal_event(1, EncodedChange, Context) ->
{bin, EncodedThriftChange} = EncodedChange,
Type = {struct, union, {tk_events_thrift, 'AuthDataChange'}},
{deserialize(Type, EncodedThriftChange), Context}.
%%
serialize(Type, Data) ->
Codec0 = thrift_strict_binary_codec:new(),
case thrift_strict_binary_codec:write(Codec0, Type, Data) of
{ok, Codec1} ->
thrift_strict_binary_codec:close(Codec1);
{error, Reason} ->
erlang:error({thrift, {protocol, Reason}})
end.
deserialize(Type, Data) ->
Codec0 = thrift_strict_binary_codec:new(Data),
case thrift_strict_binary_codec:read(Codec0, Type) of
{ok, Result, Codec1} ->
case thrift_strict_binary_codec:close(Codec1) of
<<>> ->
Result;
Leftovers ->
erlang:error({thrift, {protocol, {excess_binary_data, Leftovers}}})
end;
{error, Reason} ->
erlang:error({thrift, {protocol, Reason}})
end.
%%
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
-include_lib("token_keeper_proto/include/tk_events_thrift.hrl").
-spec test() -> _.
-spec marshal_unmarshal_created_test() -> _.
-spec marshal_unmarshal_status_changed_test() -> _.
marshal_unmarshal_created_test() ->
Event =
{created, #tk_events_AuthDataCreated{
id = <<"TEST">>,
status = active,
context = #bctx_ContextFragment{type = v1_thrift_binary, content = <<"STUFF">>},
metadata = #{}
}},
{Marshaled, _} = marshal_event(1, Event, {}),
{Unmarshaled, _} = unmarshal_event(1, Marshaled, {}),
?assertEqual(Event, Unmarshaled).
marshal_unmarshal_status_changed_test() ->
Event =
{status_changed, #tk_events_AuthDataStatusChanged{
status = revoked
}},
{Marshaled, _} = marshal_event(1, Event, {}),
{Unmarshaled, _} = unmarshal_event(1, Marshaled, {}),
?assertEqual(Event, Unmarshaled).
-endif.

106
src/tk_token.erl Normal file
View File

@ -0,0 +1,106 @@
-module(tk_token).
-export([child_specs/1]).
-export([verify/2]).
-export([issue/1]).
-callback child_spec(token_opts()) -> supervisor:child_spec().
-callback verify(token_string(), source_context()) -> {ok, token_data()} | {error, Reason :: _}.
-callback issue(token_data()) -> {ok, token_string()} | {error, Reason :: _}.
-type tokens_config() :: #{token_type() => token_opts()}.
-type token_opts() :: tk_token_jwt:opts().
-export_type([tokens_config/0]).
%%
-type token_string() :: binary().
-type token_data() :: #{
id := token_id(),
type := token_type(),
expiration := expiration(),
payload := payload(),
authority_id := authority_id(),
source_context => source_context()
}.
-type token_id() :: binary().
-type token_type() :: jwt.
-type expiration() :: unlimited | non_neg_integer().
-type payload() :: map().
-type authority_id() :: tk_authdata:authority_id().
-type source_context() :: #{
request_origin => binary()
}.
-export_type([token_string/0]).
-export_type([token_data/0]).
-export_type([token_id/0]).
-export_type([token_type/0]).
-export_type([expiration/0]).
-export_type([payload/0]).
-export_type([authority_id/0]).
-export_type([source_context/0]).
%%
-spec child_specs(tokens_config()) -> [supervisor:child_spec()].
child_specs(TokensOpts) ->
maps:fold(
fun(TokenType, TokenOpts, Acc) ->
[child_spec(TokenType, TokenOpts) | Acc]
end,
[],
TokensOpts
).
child_spec(TokenType, TokenOpts) ->
Handler = get_token_handler(TokenType),
Handler:child_spec(TokenOpts).
%%
-spec verify(token_string(), source_context()) -> {ok, token_data()} | {error, Reason :: _}.
verify(Token, SourceContext) ->
case determine_token_type(Token) of
{ok, KnownType} ->
verify(KnownType, Token, SourceContext)
% {error, unknown_token_type = Reason} ->
% {error, Reason}
end.
-spec issue(token_data()) -> {ok, token_string()} | {error, Reason :: _}.
issue(#{type := TokenType} = TokenData) ->
issue(TokenType, TokenData).
%%
%% Nothing else is defined or supported
determine_token_type(_) ->
{ok, jwt}.
verify(TokenType, Token, SourceContext) ->
Handler = get_token_handler(TokenType),
case Handler:verify(Token, SourceContext) of
{ok, VerifiedToken} ->
check_blacklist(VerifiedToken);
{error, Reason} ->
{error, {verification_failed, Reason}}
end.
check_blacklist(#{id := TokenID, authority_id := AuthorityID} = TokenData) ->
case tk_blacklist:is_blacklisted(TokenID, AuthorityID) of
false ->
{ok, TokenData};
true ->
{error, blacklisted}
end.
issue(TokenType, TokenData) ->
Handler = get_token_handler(TokenType),
Handler:issue(TokenData).
get_token_handler(jwt) ->
tk_token_jwt.

View File

@ -1,220 +1,216 @@
-module(tk_token_jwt).
-include_lib("jose/include/jose_jwk.hrl").
-include_lib("jose/include/jose_jwt.hrl").
%% API
-export([issue/3]).
-export([verify/2]).
-export([get_token_id/1]).
-export([get_subject_id/1]).
-export([get_subject_email/1]).
-export([get_expires_at/1]).
-export([get_claims/1]).
-export([get_claim/2]).
-export([get_claim/3]).
-export([get_authority/1]).
-export([get_metadata/1]).
-export([get_source_context/1]).
-export([create_claims/2]).
-export([set_subject_email/2]).
-export([get_key_authority/1]).
%% Supervisor callbacks
%%
-behaviour(supervisor).
-export([init/1]).
%%
-behaviour(tk_token).
-export([child_spec/1]).
-export([verify/2]).
-export([issue/1]).
%% API types
%%
-type t() :: {claims(), authority(), metadata()}.
-type claim() :: expiration() | term().
-type claims() :: #{binary() => claim()}.
-type token() :: binary().
-type expiration() :: unlimited | non_neg_integer().
-type options() :: #{
%% The set of keys used to sign issued tokens and verify signatures on such
%% tokens.
keyset => keyset()
-type opts() :: #{
authority_bindings := authority_bindings(),
keyset := keyset()
}.
-type metadata() :: #{
source_context => source_context()
}.
-type keyname() :: term().
-export_type([t/0]).
-export_type([claim/0]).
-export_type([claims/0]).
-export_type([token/0]).
-export_type([expiration/0]).
-export_type([metadata/0]).
-export_type([options/0]).
-export_type([keyname/0]).
%% Internal types
-type kid() :: binary().
-type key() :: #jose_jwk{}.
-type subject_id() :: binary().
-type token_id() :: binary().
-type authority() :: atom().
%??
-type source_context() :: tk_extractor_detect_token:token_source().
-type keyset() :: #{
keyname() => key_opts()
}.
-type key_name() :: binary().
-type key_opts() :: #{
source := keysource(),
authority := authority()
source := keysource()
}.
-type keysource() ::
{pem_file, file:filename()}.
-type authority_bindings() :: #{authority_id() => key_name()}.
-type keyset() :: #{key_name() => key_opts()}.
-export_type([opts/0]).
-export_type([authority_bindings/0]).
-export_type([key_name/0]).
-export_type([key_opts/0]).
-export_type([keyset/0]).
%%
-type keysource() :: {pem_file, file:filename()}.
-type authority_id() :: tk_token:authority_id().
-type source_context() :: tk_token:source_context().
-type token_data() :: tk_token:token_data().
-type token_string() :: tk_token:token_string().
%%
-define(CLAIM_TOKEN_ID, <<"jti">>).
-define(CLAIM_SUBJECT_ID, <<"sub">>).
-define(CLAIM_SUBJECT_EMAIL, <<"email">>).
-define(CLAIM_EXPIRES_AT, <<"exp">>).
-define(PTERM_KEY(Key), {?MODULE, Key}).
-define(KEY_BY_KEY_ID(KeyID), ?PTERM_KEY({key_id, KeyID})).
-define(KEY_BY_KEY_NAME(KeyName), ?PTERM_KEY({key_name, KeyName})).
-define(AUTHORITY_OF_KEY_NAME(KeyName), ?PTERM_KEY({authority_of_keyname, KeyName})).
-define(KEY_NAME_OF_AUTHORITY(AuthorityID), ?PTERM_KEY({keyname_of_authority, AuthorityID})).
%%
-spec child_spec(opts()) -> supervisor:child_spec().
child_spec(TokenOpts) ->
#{
id => ?MODULE,
start => {supervisor, start_link, [?MODULE, TokenOpts]},
type => supervisor
}.
%%
-spec init(opts()) -> {ok, {supervisor:sup_flags(), [supervisor:child_spec()]}}.
init(#{keyset := KeySet, authority_bindings := AuthorityBindings}) ->
Keys = load_keys(KeySet),
_ = assert_keys_unique(Keys),
_ = store_keys(Keys),
_ = store_authority_bindings(AuthorityBindings),
{ok, {#{}, []}}.
%% API functions
%%
-spec get_token_id(t()) -> token_id() | undefined.
get_token_id(T) ->
get_claim(?CLAIM_TOKEN_ID, T, undefined).
-spec get_subject_id(t()) -> subject_id() | undefined.
get_subject_id(T) ->
get_claim(?CLAIM_SUBJECT_ID, T, undefined).
-spec get_subject_email(t()) -> binary() | undefined.
get_subject_email(T) ->
get_claim(?CLAIM_SUBJECT_EMAIL, T, undefined).
-spec get_expires_at(t()) -> expiration() | undefined.
get_expires_at(T) ->
case get_claim(?CLAIM_EXPIRES_AT, T, undefined) of
0 -> unlimited;
V -> V
end.
-spec get_claims(t()) -> claims().
get_claims({Claims, _Authority, _Metadata}) ->
Claims.
-spec get_claim(binary(), t()) -> claim().
get_claim(ClaimName, {Claims, _Authority, _Metadata}) ->
maps:get(ClaimName, Claims).
-spec get_claim(binary(), t(), claim()) -> claim().
get_claim(ClaimName, {Claims, _Authority, _Metadata}, Default) ->
maps:get(ClaimName, Claims, Default).
-spec get_authority(t()) -> authority().
get_authority({_Claims, Authority, _Metadata}) ->
Authority.
-spec get_metadata(t()) -> metadata().
get_metadata({_Claims, _Authority, Metadata}) ->
Metadata.
-spec get_source_context(t()) -> source_context().
get_source_context({_Claims, _Authority, Metadata}) ->
maps:get(source_context, Metadata).
-spec create_claims(claims(), expiration()) -> claims().
create_claims(Claims, Expiration) ->
Claims#{?CLAIM_EXPIRES_AT => Expiration}.
-spec set_subject_email(binary(), claims()) -> claims().
set_subject_email(SubjectEmail, Claims) ->
false = maps:is_key(?CLAIM_SUBJECT_EMAIL, Claims),
Claims#{?CLAIM_SUBJECT_EMAIL => SubjectEmail}.
%%
-spec issue(token_id(), claims(), keyname()) ->
{ok, token()}
| {error, nonexistent_key}
| {error, {invalid_signee, Reason :: atom()}}.
issue(JTI, Claims, Signer) ->
case try_get_key_for_sign(Signer) of
{ok, Key} ->
FinalClaims = construct_final_claims(JTI, Claims),
sign(Key, FinalClaims);
{error, Error} ->
{error, Error}
end.
try_get_key_for_sign(Keyname) ->
case get_key_by_name(Keyname) of
#{can_sign := true} = Key ->
{ok, Key};
#{} ->
{error, {invalid_signee, signing_not_allowed}};
undefined ->
{error, nonexistent_key}
end.
construct_final_claims(JTI, Claims) ->
maps:map(fun encode_claim/2, Claims#{?CLAIM_TOKEN_ID => JTI}).
encode_claim(?CLAIM_EXPIRES_AT, Expiration) ->
mk_expires_at(Expiration);
encode_claim(_, Value) ->
Value.
mk_expires_at(unlimited) ->
0;
mk_expires_at(Dl) ->
Dl.
sign(#{kid := KID, jwk := JWK, signer := #{} = JWS}, Claims) ->
JWT = jose_jwt:sign(JWK, JWS#{<<"kid">> => KID}, Claims),
{_Modules, Token} = jose_jws:compact(JWT),
{ok, Token}.
%%
-spec verify(token(), source_context()) ->
{ok, t()}
-spec verify(token_string(), source_context()) ->
{ok, token_data()}
| {error,
{invalid_token,
badarg
| {badarg, term()}
| {missing, atom()}}
| {nonexistent_key, kid()}
| {invalid_operation, term()}
{alg_not_supported, Alg :: atom()}
| {key_not_found, KID :: atom()}
| {invalid_token, Reason :: term()}
| invalid_signature}.
verify(Token, SourceContext) ->
case do_verify(Token) of
{ok, {Claims, KeyName}} ->
{ok, construct_token_data(Claims, SourceContext, get_authority_of_key_name(KeyName))};
{error, _} = Error ->
Error
end.
-spec issue(token_data()) ->
{ok, token_string()}
| {error, issuing_not_supported | key_does_not_exist | authority_does_not_exist}.
issue(#{authority_id := AuthorityID} = TokenData) ->
case get_key_name_of_authority(AuthorityID) of
KeyName when KeyName =/= undefined ->
case get_key_by_name(KeyName) of
#{} = KeyInfo ->
case key_supports_signing(KeyInfo) of
true ->
{ok, issue_with_key(KeyInfo, TokenData)};
false ->
{error, issuing_not_supported}
end;
undefined ->
{error, key_does_not_exist}
end;
undefined ->
{error, authority_does_not_exist}
end.
%% Internal functions
load_keys(KeySet) ->
maps:fold(fun load_key/3, [], KeySet).
load_key(KeyName, KeyOpts, Acc) ->
Source = maps:get(source, KeyOpts),
case load_key_from_source(Source) of
{ok, KeyID, JWK} ->
[construct_key(KeyID, JWK, KeyName) | Acc];
{error, Reason} ->
exit({import_error, Source, Reason})
end.
derive_kid_from_public_key_pem_entry(JWK) ->
JWKPublic = jose_jwk:to_public(JWK),
{_Module, PublicKey} = JWKPublic#jose_jwk.kty,
{_PemEntry, Data, _} = public_key:pem_entry_encode('SubjectPublicKeyInfo', PublicKey),
jose_base64url:encode(crypto:hash(sha256, Data)).
load_key_from_source({pem_file, Filename}) ->
case jose_jwk:from_pem_file(Filename) of
JWK = #jose_jwk{} ->
KID = derive_kid_from_public_key_pem_entry(JWK),
{ok, KID, JWK};
Error = {error, _} ->
Error
end.
construct_key(KeyID, JWK, KeyName) ->
#{
jwk => JWK,
key_id => KeyID,
key_name => KeyName,
verifier => get_verifier(JWK),
signer => get_signer(JWK)
}.
get_signer(JWK) ->
try
jose_jwk:signer(JWK)
catch
error:_ ->
undefined
end.
get_verifier(JWK) ->
try
jose_jwk:verifier(JWK)
catch
error:_ ->
undefined
end.
%%
assert_keys_unique(KeyInfos) ->
lists:foldr(fun assert_key_unique/2, [], KeyInfos).
assert_key_unique(#{key_id := KeyID, key_name := KeyName}, SeenKeyIDs) ->
case lists:member(KeyID, SeenKeyIDs) of
true -> exit({import_error, {duplicate_kid, KeyName}});
false -> [KeyID | SeenKeyIDs]
end.
%%
store_keys(KeyInfos) ->
lists:foreach(fun store_key/1, KeyInfos).
store_key(#{key_id := KeyID, key_name := KeyName} = KeyInfo) ->
put_key(KeyID, KeyName, KeyInfo).
%% Verifying
do_verify(Token) ->
try
{_, ExpandedToken} = jose_jws:expand(Token),
#{<<"protected">> := ProtectedHeader} = ExpandedToken,
Header = base64url_to_map(ProtectedHeader),
Alg = get_alg(Header),
KID = get_kid(Header),
verify(KID, Alg, ExpandedToken, SourceContext)
KeyID = get_key_id(Header),
case get_key_by_id(KeyID) of
#{} = KeyInfo ->
case key_supports_verification(Alg, KeyInfo) of
true ->
verify_with_key(ExpandedToken, KeyInfo);
false ->
{error, {alg_not_supported, Alg}}
end;
undefined ->
{error, {key_not_found, KeyID}}
end
catch
%% from get_alg and get_kid
throw:Reason ->
throw:({invalid_token, {missing, _}} = Reason) ->
{error, Reason};
%% TODO we're losing error information here, e.g. stacktrace
error:Reason ->
error:{badarg, Reason} ->
{error, {invalid_token, Reason}}
end.
@ -222,27 +218,109 @@ base64url_to_map(Base64) when is_binary(Base64) ->
{ok, Json} = jose_base64url:decode(Base64),
jsx:decode(Json, [return_maps]).
verify(KID, Alg, ExpandedToken, SourceContext) ->
case get_key_by_kid(KID) of
#{jwk := JWK, verifier := Algs, authority := Authority} ->
_ = lists:member(Alg, Algs) orelse throw({invalid_operation, Alg}),
verify_with_key(JWK, ExpandedToken, Authority, make_metadata(SourceContext));
undefined ->
{error, {nonexistent_key, KID}}
end.
get_key_id(#{<<"kid">> := KID}) when is_binary(KID) ->
KID;
get_key_id(#{}) ->
throw({invalid_token, {missing, kid}}).
make_metadata(SourceContext) ->
#{source_context => SourceContext}.
get_alg(#{<<"alg">> := Alg}) when is_binary(Alg) ->
Alg;
get_alg(#{}) ->
throw({invalid_token, {missing, alg}}).
verify_with_key(JWK, ExpandedToken, Authority, Metadata) ->
key_supports_verification(Alg, #{verifier := Algs}) ->
lists:member(Alg, Algs).
verify_with_key(ExpandedToken, #{jwk := JWK, key_name := KeyName}) ->
case jose_jwt:verify(JWK, ExpandedToken) of
{true, #jose_jwt{fields = Claims}, _JWS} ->
_ = validate_claims(Claims),
{ok, {Claims, Authority, Metadata}};
{ok, {Claims, KeyName}};
{false, _JWT, _JWS} ->
{error, invalid_signature}
end.
%%
construct_token_data(Claims, SourceContext, AuthorityID) ->
#{
id => maps:get(?CLAIM_TOKEN_ID, Claims),
type => jwt,
expiration => decode_expiration(maps:get(?CLAIM_EXPIRES_AT, Claims)),
payload => Claims,
authority_id => AuthorityID,
source_context => SourceContext
}.
decode_expiration(0) ->
unlimited;
decode_expiration(Expiration) when is_integer(Expiration) ->
Expiration.
%% Signing
key_supports_signing(#{signer := #{}}) ->
true;
key_supports_signing(#{signer := undefined}) ->
false.
issue_with_key(#{key_id := KeyID, jwk := JWK, signer := #{} = JWS}, TokenData) ->
Claims = construct_claims(TokenData),
JWT = jose_jwt:sign(JWK, JWS#{<<"kid">> => KeyID}, Claims),
{_Modules, Token} = jose_jws:compact(JWT),
Token.
construct_claims(#{id := TokenID, expiration := Expiration, payload := Claims}) ->
maps:map(fun encode_claim/2, Claims#{
?CLAIM_TOKEN_ID => TokenID,
?CLAIM_EXPIRES_AT => Expiration
}).
encode_claim(?CLAIM_EXPIRES_AT, Expiration) ->
encode_expires_at(Expiration);
encode_claim(_, Value) ->
Value.
encode_expires_at(unlimited) ->
0;
encode_expires_at(Dl) ->
Dl.
%%
put_key(KeyID, KeyName, KeyInfo) ->
%% Official [Erlang Reference Manual](https://www.erlang.org/doc/man/persistent_term.html) recommends
%% storing one big persistent_term over muliple small ones, reasoning being that "the execution time for storing
%% a persistent term is proportional to the number of already existing terms". Since we literally never add new
%% keys after initial configuration at application start, it seems fine to do this here.
ok = persistent_term:put(?KEY_BY_KEY_ID(KeyID), KeyInfo),
ok = persistent_term:put(?KEY_BY_KEY_NAME(KeyName), KeyInfo),
ok.
get_key_by_id(KeyID) ->
persistent_term:get(?KEY_BY_KEY_ID(KeyID), undefined).
get_key_by_name(KeyName) ->
persistent_term:get(?KEY_BY_KEY_NAME(KeyName), undefined).
%%
store_authority_bindings(AuthorityBindings) ->
maps:foreach(fun put_authority_binding/2, AuthorityBindings).
put_authority_binding(KeyName, AuthorityID) ->
ok = persistent_term:put(?AUTHORITY_OF_KEY_NAME(KeyName), AuthorityID),
ok = persistent_term:put(?KEY_NAME_OF_AUTHORITY(AuthorityID), KeyName),
ok.
get_authority_of_key_name(KeyName) ->
persistent_term:get(?AUTHORITY_OF_KEY_NAME(KeyName), undefined).
get_key_name_of_authority(AuthorityID) ->
persistent_term:get(?KEY_NAME_OF_AUTHORITY(AuthorityID), undefined).
%%
validate_claims(Claims) ->
validate_claims(Claims, get_validators()).
@ -252,16 +330,6 @@ validate_claims(Claims, [{Name, Claim, Validator} | Rest]) ->
validate_claims(Claims, []) ->
Claims.
get_kid(#{<<"kid">> := KID}) when is_binary(KID) ->
KID;
get_kid(#{}) ->
throw({invalid_token, {missing, kid}}).
get_alg(#{<<"alg">> := Alg}) when is_binary(Alg) ->
Alg;
get_alg(#{}) ->
throw({invalid_token, {missing, alg}}).
get_validators() ->
[
{token_id, ?CLAIM_TOKEN_ID, fun check_presence/2}
@ -273,161 +341,3 @@ check_presence(_, V) when is_integer(V) ->
V;
check_presence(C, undefined) ->
throw({invalid_token, {missing, C}}).
%%
-spec get_key_authority(keyname()) -> {ok, authority()} | {error, {nonexistent_key, keyname()}}.
get_key_authority(KeyName) ->
case get_key_by_name(KeyName) of
#{authority := Authority} ->
{ok, Authority};
undefined ->
{error, {nonexistent_key, KeyName}}
end.
%%
%% Supervisor callbacks
%%
-spec child_spec(options()) -> supervisor:child_spec() | no_return().
child_spec(Options) ->
#{
id => ?MODULE,
start => {supervisor, start_link, [?MODULE, parse_options(Options)]},
type => supervisor
}.
parse_options(Options) ->
Keyset = maps:get(keyset, Options, #{}),
_ = is_map(Keyset) orelse exit({invalid_option, keyset, Keyset}),
_ = genlib_map:foreach(
fun(KeyName, KeyOpts = #{source := Source}) ->
Authority = maps:get(authority, KeyOpts),
_ =
is_keysource(Source) orelse
exit({invalid_source, KeyName, Source}),
_ =
is_atom(Authority) orelse
exit({invalid_authority, KeyName, Authority})
end,
Keyset
),
Keyset.
is_keysource({pem_file, Fn}) ->
is_list(Fn) orelse is_binary(Fn);
is_keysource(_) ->
false.
%%
-spec init(keyset()) -> {ok, {supervisor:sup_flags(), [supervisor:child_spec()]}}.
init(Keyset) ->
ok = create_table(),
_ = maps:map(fun ensure_store_key/2, Keyset),
{ok, {#{}, []}}.
ensure_store_key(KeyName, KeyOpts) ->
Source = maps:get(source, KeyOpts),
Authority = maps:get(authority, KeyOpts),
case store_key(KeyName, Source, Authority) of
ok ->
ok;
{error, Reason} ->
exit({import_error, KeyName, Source, Reason})
end.
-spec store_key(keyname(), {pem_file, file:filename()}, authority()) ->
ok | {error, file:posix() | {unknown_key | duplicate_key, _}}.
store_key(Keyname, {pem_file, Filename}, Authority) ->
store_key(Keyname, {pem_file, Filename}, Authority, #{
kid => fun derive_kid_from_public_key_pem_entry/1
}).
derive_kid_from_public_key_pem_entry(JWK) ->
JWKPublic = jose_jwk:to_public(JWK),
{_Module, PublicKey} = JWKPublic#jose_jwk.kty,
{_PemEntry, Data, _} = public_key:pem_entry_encode('SubjectPublicKeyInfo', PublicKey),
jose_base64url:encode(crypto:hash(sha256, Data)).
-type store_opts() :: #{
kid => fun((key()) -> kid())
}.
-spec store_key(keyname(), {pem_file, file:filename()}, authority(), store_opts()) ->
ok | {error, file:posix() | {unknown_key | duplicate_key, _}}.
store_key(Keyname, {pem_file, Filename}, Authority, Opts) ->
case jose_jwk:from_pem_file(Filename) of
JWK = #jose_jwk{} ->
Key = construct_key(derive_kid(JWK, Opts), JWK),
insert_unique_key(Keyname, Key#{authority => Authority});
Error = {error, _} ->
Error
end.
derive_kid(JWK, #{kid := DeriveFun}) when is_function(DeriveFun, 1) ->
DeriveFun(JWK).
construct_key(KID, JWK) ->
Signer =
try
jose_jwk:signer(JWK)
catch
error:_ -> undefined
end,
Verifier =
try
jose_jwk:verifier(JWK)
catch
error:_ -> undefined
end,
#{
jwk => JWK,
kid => KID,
signer => Signer,
can_sign => Signer /= undefined,
verifier => Verifier,
can_verify => Verifier /= undefined
}.
insert_unique_key(Keyname, KeyInfo = #{kid := KID}) ->
case get_key_by_kid(KID) of
undefined ->
insert_key(Keyname, KeyInfo);
_ ->
{error, {duplicate_key, Keyname}}
end.
insert_key(Keyname, KeyInfo = #{kid := KID}) ->
insert_values(#{
{keyname, Keyname} => KeyInfo,
{kid, KID} => KeyInfo
}).
%%
%% Internal functions
%%
get_key_by_name(Keyname) ->
lookup_value({keyname, Keyname}).
get_key_by_kid(KID) ->
lookup_value({kid, KID}).
-define(TABLE, ?MODULE).
create_table() ->
_ = ets:new(?TABLE, [set, public, named_table, {read_concurrency, true}]),
ok.
insert_values(Values) ->
true = ets:insert(?TABLE, maps:to_list(Values)),
ok.
lookup_value(Key) ->
case ets:lookup(?TABLE, Key) of
[{Key, Value}] ->
Value;
[] ->
undefined
end.

View File

@ -1,239 +0,0 @@
-module(tk_woody_handler).
-include_lib("token_keeper_proto/include/tk_context_thrift.hrl").
-include_lib("token_keeper_proto/include/tk_token_keeper_thrift.hrl").
%% Woody handler
-behaviour(woody_server_thrift_handler).
-export([handle_function/4]).
-type handle_ctx() :: #{
woody_ctx := woody_context:ctx()
}.
-export_type([handle_ctx/0]).
%% Internal types
-type opts() :: #{
pulse => tk_pulse:handlers()
}.
-record(state, {
woody_context :: woody_context:ctx(),
pulse :: tk_pulse:handlers(),
pulse_metadata :: tk_pulse:metadata()
}).
%%
-spec handle_function(woody:func(), woody:args(), woody_context:ctx(), opts()) -> {ok, woody:result()} | no_return().
handle_function(Op, Args, WoodyCtx, Opts) ->
State = make_state(WoodyCtx, Opts),
handle_function_(Op, Args, State).
handle_function_('Create' = Op, {ID, ContextFragment, Metadata}, State) ->
%% Create - создает новую AuthData, используя переданные в качестве
%% аргументов данные и сохраняет их в хранилище, после чего выписывает
%% новый JWT-токен, в котором содержится AuthDataID (на данный момент
%% предполагается, что AuthDataID == jwt-клейму JTI). По умолчанию
%% status токена - active; authority - id выписывающей authority.
_ = handle_beat(Op, started, State),
AuthorityConf = get_autority_config(get_issuing_authority()),
AuthData = issue_auth_data(ID, ContextFragment, Metadata, AuthorityConf),
case store(AuthData, build_context(State)) of
ok ->
{ok, Token} = tk_token_jwt:issue(ID, #{}, get_signer(AuthorityConf)),
EncodedAuthData = encode_auth_data(AuthData#{token => Token}),
_ = handle_beat(Op, succeeded, State),
{ok, EncodedAuthData};
{error, exists} ->
_ = handle_beat(Op, {failed, exists}, State),
woody_error:raise(business, #token_keeper_AuthDataAlreadyExists{})
end;
handle_function_('CreateEphemeral' = Op, {ContextFragment, Metadata}, State) ->
_ = handle_beat(Op, started, State),
AuthorityConf = get_autority_config(get_issuing_authority()),
AuthData = issue_auth_data(ContextFragment, Metadata, AuthorityConf),
Claims = tk_token_claim_utils:encode_authdata(AuthData),
{ok, Token} = tk_token_jwt:issue(unique_id(), Claims, get_signer(AuthorityConf)),
EncodedAuthData = encode_auth_data(AuthData#{token => Token}),
_ = handle_beat(Op, succeeded, State),
{ok, EncodedAuthData};
handle_function_('AddExistingToken', _, _State) ->
erlang:error(not_implemented);
handle_function_('GetByToken' = Op, {Token, TokenSourceContext}, State) ->
_ = handle_beat(Op, started, State),
TokenSourceContextDecoded = decode_source_context(TokenSourceContext),
case verify_token(Token, TokenSourceContextDecoded) of
{ok, TokenInfo} ->
State1 = save_pulse_metadata(#{token => TokenInfo}, State),
{_, Authority} = get_autority_config(get_token_authority(TokenInfo)),
case tk_authority:get_authdata_by_token(TokenInfo, Authority, build_context(State)) of
{ok, AuthDataPrototype} ->
EncodedAuthData = encode_auth_data(AuthDataPrototype#{token => Token}),
_ = handle_beat(Op, succeeded, State1),
{ok, EncodedAuthData};
{error, Reason} ->
_ = handle_beat(Op, {failed, Reason}, State1),
woody_error:raise(business, #token_keeper_AuthDataNotFound{})
end;
{error, {verification, Reason}} ->
_ = handle_beat(Op, {failed, {token_verification, Reason}}, State),
woody_error:raise(business, #token_keeper_InvalidToken{});
{error, blacklisted} ->
_ = handle_beat(Op, {failed, blacklisted}, State),
woody_error:raise(business, #token_keeper_AuthDataRevoked{})
end;
handle_function_('Get' = Op, {ID}, State) ->
_ = handle_beat(Op, started, State),
case get_authdata_by_id(ID, build_context(State)) of
{ok, AuthData} ->
EncodedAuthData = encode_auth_data(AuthData),
_ = handle_beat(Op, succeeded, State),
{ok, EncodedAuthData};
{error, Reason} ->
_ = handle_beat(Op, {failed, Reason}, State),
woody_error:raise(business, #token_keeper_AuthDataNotFound{})
end;
handle_function_('Revoke' = Op, {ID}, State) ->
_ = handle_beat(Op, started, State),
case revoke(ID, build_context(State)) of
ok ->
_ = handle_beat(Op, succeeded, State),
{ok, ok};
{error, notfound} ->
_ = handle_beat(Op, {failed, notfound}, State),
woody_error:raise(business, #token_keeper_AuthDataNotFound{})
end.
%% Internal functions
verify_token(Token, TokenSourceContextDecoded) ->
case tk_token_jwt:verify(Token, TokenSourceContextDecoded) of
{ok, TokenInfo} ->
case check_blacklist(TokenInfo) of
false ->
{ok, TokenInfo};
true ->
{error, blacklisted}
end;
{error, Reason} ->
{error, {verification, Reason}}
end.
check_blacklist(TokenInfo) ->
tk_token_blacklist:is_blacklisted(get_token_id(TokenInfo), get_token_authority(TokenInfo)).
%%
issue_auth_data(ContextFragment, Metadata, AuthorityConf) ->
issue_auth_data(undefined, ContextFragment, Metadata, AuthorityConf).
issue_auth_data(ID, ContextFragment, Metadata, {_, Authority}) ->
tk_authority:create_authdata(ID, ContextFragment, Metadata, Authority).
unique_id() ->
<<ID:64>> = snowflake:new(),
genlib_format:format_int_base(ID, 62).
%%
build_context(#state{woody_context = WC}) ->
#{woody_ctx => WC}.
make_state(WoodyCtx, Opts) ->
#state{
woody_context = WoodyCtx,
pulse = maps:get(pulse, Opts, []),
pulse_metadata = #{woody_ctx => WoodyCtx}
}.
encode_auth_data(AuthData) ->
#token_keeper_AuthData{
id = maps:get(id, AuthData, undefined),
token = maps:get(token, AuthData, undefined),
status = maps:get(status, AuthData),
context = maps:get(context, AuthData),
metadata = maps:get(metadata, AuthData, #{}),
authority = maps:get(authority, AuthData, undefined)
}.
decode_source_context(TokenSourceContext) ->
genlib_map:compact(#{
request_origin => TokenSourceContext#token_keeper_TokenSourceContext.request_origin
}).
%%
get_token_id(TokenInfo) ->
tk_token_jwt:get_token_id(TokenInfo).
get_token_authority(TokenInfo) ->
tk_token_jwt:get_authority(TokenInfo).
get_autority_config(AuthorityID) ->
Authorities = application:get_env(token_keeper, authorities, #{}),
case maps:get(AuthorityID, Authorities, undefined) of
Config when Config =/= undefined ->
{AuthorityID, Config};
undefined ->
throw({misconfiguration, {no_such_authority, AuthorityID}})
end.
get_issuing_authority() ->
maps:get(authority, get_issuing_config()).
get_issuing_config() ->
case application:get_env(token_keeper, issuing, undefined) of
Config when Config =/= undefined ->
Config;
undefined ->
error({misconfiguration, {issuing, not_configured}})
end.
%%
get_signer({AuthorityID, AuthorityConf}) ->
SignerKID = tk_authority:get_signer(AuthorityConf),
case tk_token_jwt:get_key_authority(SignerKID) of
{ok, AuthorityID} ->
SignerKID;
{ok, OtherAuthorityID} ->
error({misconfiguration, {issuing, {key_ownership_error, {AuthorityID, OtherAuthorityID}}}});
_ ->
error({misconfiguration, {issuing, {no_key, SignerKID}}})
end.
%%
get_authdata_by_id(ID, Ctx) ->
tk_storage:get(ID, Ctx).
store(AuthData, Ctx) ->
tk_storage:store(AuthData, Ctx).
revoke(ID, Ctx) ->
tk_storage:revoke(ID, Ctx).
%%
handle_beat(Op, Event, State) ->
tk_pulse:handle_beat({encode_pulse_op(Op), Event}, State#state.pulse_metadata, State#state.pulse).
save_pulse_metadata(Metadata, State = #state{pulse_metadata = PulseMetadata}) ->
State#state{pulse_metadata = maps:merge(Metadata, PulseMetadata)}.
encode_pulse_op('CreateEphemeral') ->
create_ephemeral;
encode_pulse_op('GetByToken') ->
get_by_token;
encode_pulse_op('Create') ->
create;
encode_pulse_op('Get') ->
get;
encode_pulse_op('Revoke') ->
revoke.

View File

@ -12,18 +12,6 @@
-export([start_link/0]).
-export([init/1]).
%% API Types
-type token() :: binary().
-type token_type() :: api_key_token | user_session_token.
-type token_source() :: #{
request_origin => binary()
}.
-export_type([token/0]).
-export_type([token_type/0]).
-export_type([token_source/0]).
%%
-define(SERVER, ?MODULE).
@ -51,8 +39,10 @@ start_link() ->
-spec init(Args :: term()) -> genlib_gen:supervisor_ret().
init([]) ->
{AuditChildSpecs, AuditPulse} = get_audit_specs(),
ServiceOpts = genlib_app:env(?MODULE, services, #{}),
EventHandlers = genlib_app:env(?MODULE, woody_event_handlers, [woody_event_handler_default]),
TokenBlacklistSpec = tk_blacklist:child_spec(genlib_app:env(?MODULE, blacklist, #{})),
TokensSpecs = tk_token:child_specs(genlib_app:env(?MODULE, tokens, #{})),
StoragesSpecs = tk_storage:child_specs(genlib_app:env(?MODULE, storages, #{})),
HandlerChildSpec = woody_server:child_spec(
?MODULE,
#{
@ -62,19 +52,69 @@ init([]) ->
transport_opts => get_transport_opts(),
shutdown_timeout => get_shutdown_timeout(),
event_handler => EventHandlers,
handlers => get_handler_specs(ServiceOpts, AuditPulse),
additional_routes => get_additional_routes(EventHandlers)
handlers => get_woody_handlers(AuditPulse),
additional_routes => [get_health_route() | get_machinegun_processor_routes(EventHandlers)]
}
),
TokensOpts = genlib_app:env(?MODULE, jwt, #{}),
TokensChildSpec = tk_token_jwt:child_spec(TokensOpts),
TokenBlacklistOpts = genlib_app:env(?MODULE, blacklist, #{}),
TokenBlacklistSpec = tk_token_blacklist:child_spec(TokenBlacklistOpts),
{ok,
{
#{strategy => one_for_all, intensity => 6, period => 30},
[HandlerChildSpec, TokensChildSpec, TokenBlacklistSpec | AuditChildSpecs]
}}.
{ok, {
#{strategy => one_for_all, intensity => 6, period => 30},
lists:flatten([
AuditChildSpecs,
TokenBlacklistSpec,
TokensSpecs,
StoragesSpecs,
HandlerChildSpec
])
}}.
%%
-spec get_woody_handlers(tk_pulse:handlers()) -> [woody:http_handler(woody:th_handler())].
get_woody_handlers(AuditPulse) ->
lists:flatten([
get_authenticator_handler(genlib_app:env(?MODULE, authenticator, #{}), AuditPulse),
get_authority_handler(genlib_app:env(?MODULE, authorities, #{}), AuditPulse)
]).
get_authenticator_handler(Config, AuditPulse) ->
tk_handler:get_authenticator_handler(Config, AuditPulse).
get_authority_handler(Authorities, AuditPulse) ->
maps:fold(
fun(AuthorityID, AuthorityOpts, Acc) ->
[tk_handler:get_authority_handler(AuthorityID, AuthorityOpts, AuditPulse) | Acc]
end,
[],
Authorities
).
%%
-spec get_machinegun_processor_routes(woody:ev_handlers()) -> [woody_server_thrift_v2:route(_)].
get_machinegun_processor_routes(EventHandlers) ->
case genlib_app:env(?MODULE, machinegun, #{}) of
#{processor := ProcessorConf} ->
tk_storage_machinegun:get_routes(ProcessorConf, #{event_handler => EventHandlers});
#{} ->
[]
end.
%%
-spec get_health_route() -> woody_server_thrift_v2:route(_).
get_health_route() ->
Check = enable_health_logging(genlib_app:env(?MODULE, health_check, #{})),
erl_health_handle:get_route(Check).
-spec enable_health_logging(erl_health:check()) -> erl_health:check().
enable_health_logging(Check) ->
EvHandler = {erl_health_event_handler, []},
maps:map(
fun(_, Runner) -> #{runner => Runner, event_handler => EvHandler} end,
Check
).
%%
-spec get_ip_address() -> inet:ip_address().
get_ip_address() ->
@ -97,18 +137,6 @@ get_transport_opts() ->
get_shutdown_timeout() ->
genlib_app:env(?MODULE, shutdown_timeout, 0).
-spec get_handler_specs(map(), tk_pulse:handlers()) -> [woody:http_handler(woody:th_handler())].
get_handler_specs(ServiceOpts, AuditPulse) ->
TokenKeeperService = maps:get(token_keeper, ServiceOpts, #{}),
TokenKeeperPulse = maps:get(pulse, TokenKeeperService, []),
TokenKeeperOpts = #{pulse => AuditPulse ++ TokenKeeperPulse},
[
{
maps:get(path, TokenKeeperService, <<"/v1/token-keeper">>),
{{tk_token_keeper_thrift, 'TokenKeeper'}, {tk_woody_handler, TokenKeeperOpts}}
}
].
-spec get_audit_specs() -> {[supervisor:child_spec()], tk_pulse:handlers()}.
get_audit_specs() ->
Opts = genlib_app:env(?MODULE, audit, #{}),
@ -119,25 +147,3 @@ get_audit_specs() ->
disable ->
{[], []}
end.
-spec get_additional_routes(woody:ev_handlers()) -> machinery_utils:woody_routes().
get_additional_routes(EventHandlers) ->
Check = enable_health_logging(genlib_app:env(?MODULE, health_check, #{})),
HealthRoute = erl_health_handle:get_route(Check),
case genlib_app:env(?MODULE, storage) of
%% TODO: Better storage initialization
{machinegun, _} ->
[HealthRoute | tk_storage_machinegun:get_routes(#{event_handler => EventHandlers})];
_ ->
[HealthRoute]
end.
%%
-spec enable_health_logging(erl_health:check()) -> erl_health:check().
enable_health_logging(Check) ->
EvHandler = {erl_health_event_handler, []},
maps:map(
fun(_, Runner) -> #{runner => Runner, event_handler => EvHandler} end,
Check
).

View File

@ -1,901 +0,0 @@
-module(tk_tests_SUITE).
-include_lib("common_test/include/ct.hrl").
-include_lib("stdlib/include/assert.hrl").
-include_lib("jose/include/jose_jwk.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_context_v1_thrift.hrl").
-export([all/0]).
-export([groups/0]).
-export([init_per_suite/1]).
-export([end_per_suite/1]).
-export([init_per_group/2]).
-export([end_per_group/2]).
-export([init_per_testcase/2]).
-export([end_per_testcase/2]).
-export([detect_api_key_test/1]).
-export([detect_user_session_token_test/1]).
-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]).
-export([basic_issuing_test/1]).
-export([jti_and_authority_blacklist_test/1]).
-export([empty_blacklist_test/1]).
-export([simple_create_test/1]).
-export([create_twice_test/1]).
-export([revoke_twice_test/1]).
-export([revoke_notexisted_test/1]).
-export([get_notexisted_test/1]).
-export([getbytoken_test/1]).
-type config() :: ct_helper:config().
-type group_name() :: atom().
-type test_case_name() :: atom().
-define(CONFIG(Key, C), (element(2, lists:keyfind(Key, 1, C)))).
-define(META_PARTY_ID, <<"test.rbkmoney.party.id">>).
-define(META_USER_ID, <<"test.rbkmoney.user.id">>).
-define(META_USER_EMAIL, <<"test.rbkmoney.user.email">>).
-define(META_USER_REALM, <<"test.rbkmoney.user.realm">>).
-define(META_CAPI_CONSUMER, <<"test.rbkmoney.capi.consumer">>).
-define(TK_AUTHORITY_KEYCLOAK, <<"test.rbkmoney.keycloak">>).
-define(TK_AUTHORITY_CAPI, <<"test.rbkmoney.capi">>).
-define(TK_RESOURCE_DOMAIN, <<"test-domain">>).
-define(TOKEN_SOURCE_CONTEXT(), ?TOKEN_SOURCE_CONTEXT(<<"http://spanish.inquisition">>)).
-define(TOKEN_SOURCE_CONTEXT(SourceURL), #token_keeper_TokenSourceContext{request_origin = SourceURL}).
-define(USER_TOKEN_SOURCE, <<"https://dashboard.rbk.money">>).
-define(CTX_ENTITY(ID), #bctx_v1_Entity{id = ID}).
%%
-spec all() -> [atom()].
all() ->
[
{group, detect_token_type},
{group, claim_only},
{group, invoice_template_access_token},
{group, issuing},
{group, blacklist},
{group, others}
].
-spec groups() -> [{group_name(), list(), [test_case_name()]}].
groups() ->
[
{detect_token_type, [parallel], [
detect_api_key_test,
detect_user_session_token_test,
detect_dummy_token_test
]},
{claim_only, [parallel], [
no_token_claim_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
]},
{issuing, [parallel], [
basic_issuing_test
]},
{blacklist, [], [
jti_and_authority_blacklist_test,
empty_blacklist_test
]},
{others, [parallel], [
simple_create_test,
create_twice_test,
revoke_twice_test,
revoke_notexisted_test,
get_notexisted_test,
getbytoken_test
]}
].
-spec init_per_suite(config()) -> config().
init_per_suite(C) ->
Apps =
genlib_app:start_application(woody) ++
genlib_app:start_application_with(scoper, [
{storage, scoper_storage_logger}
]),
[{suite_apps, Apps} | C].
-spec end_per_suite(config()) -> ok.
end_per_suite(C) ->
genlib_app:stop_unload_applications(?CONFIG(suite_apps, C)).
-spec init_per_group(group_name(), config()) -> config().
init_per_group(detect_token_type = Name, C) ->
start_keeper([
{jwt, #{
keyset => #{
test => #{
source => {pem_file, get_filename("keys/local/private.pem", C)},
authority => keycloak
}
}
}},
{authorities, #{
keycloak => #{
id => ?TK_AUTHORITY_KEYCLOAK,
authdata_sources => [
{extract, #{
methods => [
{detect_token, #{
phony_api_key_opts => #{
metadata_mappings => #{
party_id => ?META_PARTY_ID
}
},
user_session_token_opts => #{
user_realm => <<"external">>,
metadata_mappings => #{
user_id => ?META_USER_ID,
user_email => ?META_USER_EMAIL,
user_realm => ?META_USER_REALM
}
},
user_session_token_origins => [?USER_TOKEN_SOURCE]
}}
]
}}
]
}
}}
]) ++
[{groupname, Name} | C];
init_per_group(claim_only = Name, C) ->
start_keeper([
{jwt, #{
keyset => #{
test => #{
source => {pem_file, get_filename("keys/local/private.pem", C)},
authority => claim_only
}
}
}},
{authorities, #{
claim_only => #{
id => ?TK_AUTHORITY_CAPI,
authdata_sources => [
{claim, #{
compatibility =>
{true, #{
metadata_mappings => #{
party_id => ?META_PARTY_ID,
consumer => ?META_CAPI_CONSUMER
}
}}
}}
]
}
}}
]) ++
[{groupname, Name} | C];
init_per_group(invoice_template_access_token = Name, C) ->
start_keeper([
{jwt, #{
keyset => #{
test => #{
source => {pem_file, get_filename("keys/local/private.pem", C)},
authority => invoice_tpl_authority
}
}
}},
{authorities, #{
invoice_tpl_authority => #{
id => ?TK_AUTHORITY_CAPI,
authdata_sources => [
{claim, #{
compatibility =>
{true, #{
metadata_mappings => #{
party_id => ?META_PARTY_ID,
token_consumer => ?META_CAPI_CONSUMER
}
}}
}},
{extract, #{
methods => [
{invoice_template_access_token, #{
domain => ?TK_RESOURCE_DOMAIN,
metadata_mappings => #{
party_id => ?META_PARTY_ID
}
}}
]
}}
]
}
}}
]) ++
[{groupname, Name} | C];
init_per_group(issuing = Name, C) ->
start_keeper([
{jwt, #{
keyset => #{
test => #{
source => {pem_file, get_filename("keys/local/private.pem", C)},
authority => issuing_authority
}
}
}},
{issuing, #{
authority => issuing_authority
}},
{authorities, #{
issuing_authority => #{
id => ?TK_AUTHORITY_CAPI,
signer => test,
authdata_sources => [
claim
]
}
}}
]) ++
[{groupname, Name} | C];
init_per_group(others = Name, C) ->
start_keeper([
{jwt, #{
keyset => #{
test => #{
source => {pem_file, get_filename("keys/local/private.pem", C)},
authority => issuing_authority
}
}
}},
{issuing, #{
authority => issuing_authority
}},
{storage, {machinegun, #{}}},
{service_clients, #{
automaton => #{
url => <<"http://machinegun:8022/v1/automaton">>
}
}},
{authorities, #{
issuing_authority => #{
id => ?TK_AUTHORITY_CAPI,
signer => test,
authdata_sources => [
{storage, #{}},
claim
]
}
}}
]) ++ [{groupname, Name} | C];
init_per_group(Name, C) ->
[{groupname, Name} | C].
-spec end_per_group(group_name(), config()) -> _.
end_per_group(blacklist, _C) ->
ok;
end_per_group(_GroupName, C) ->
ok = stop_keeper(C),
ok.
-spec init_per_testcase(atom(), config()) -> config().
init_per_testcase(jti_and_authority_blacklist_test = Name, C) ->
start_keeper([
{jwt, #{
keyset => #{
primary => #{
source => {pem_file, get_filename("keys/local/private.pem", C)},
authority => blacklisting_authority
},
secondary => #{
source => {pem_file, get_filename("keys/secondary/private.pem", C)},
authority => some_other_authority
}
}
}},
{blacklist, #{
path => get_filename("blacklisted_keys.yaml", C)
}},
{authorities, #{
blacklisting_authority => #{
id => ?TK_AUTHORITY_CAPI,
signer => primary,
authdata_sources => []
},
some_other_authority => #{
id => ?TK_AUTHORITY_CAPI,
signer => secondary,
authdata_sources => []
}
}}
]) ++ [{testcase, Name} | C];
init_per_testcase(empty_blacklist_test = Name, C) ->
start_keeper([
{jwt, #{
keyset => #{
primary => #{
source => {pem_file, get_filename("keys/local/private.pem", C)},
authority => authority
}
}
}},
{blacklist, #{
path => get_filename("empty_blacklist.yaml", C)
}},
{authorities, #{
authority => #{
id => ?TK_AUTHORITY_CAPI,
signer => primary,
authdata_sources => []
}
}}
]) ++ [{testcase, Name} | C];
init_per_testcase(Name, C) ->
[{testcase, Name} | C].
-spec end_per_testcase(atom(), config()) -> config().
end_per_testcase(Name, C) when
Name =:= jti_and_authority_blacklist_test;
Name =:= empty_blacklist_test
->
ok = stop_keeper(C),
ok;
end_per_testcase(_Name, _C) ->
ok.
start_keeper(Env) ->
IP = "127.0.0.1",
Port = 8022,
Path = <<"/v1/token-keeper">>,
Apps = genlib_app:start_application_with(
token_keeper,
[
{port, Port},
{services, #{
token_keeper => #{
path => Path
}
}}
] ++ Env
),
Services = #{
token_keeper => mk_url(IP, Port, Path)
},
[{group_apps, Apps}, {service_urls, Services}].
mk_url(IP, Port, Path) ->
iolist_to_binary(["http://", IP, ":", genlib:to_binary(Port), Path]).
stop_keeper(C) ->
genlib_app:stop_unload_applications(?CONFIG(group_apps, C)).
%%
-spec detect_api_key_test(config()) -> ok.
detect_api_key_test(C) ->
Client = mk_client(C),
JTI = unique_id(),
SubjectID = <<"TEST">>,
{ok, Token} = issue_token(JTI, #{<<"sub">> => SubjectID}, unlimited),
#token_keeper_AuthData{
id = undefined,
token = Token,
status = active,
context = Context,
metadata = #{?META_PARTY_ID := 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) ->
Client = mk_client(C),
JTI = unique_id(),
SubjectID = <<"TEST">>,
SubjectEmail = <<"test@test.test">>,
{ok, Token} = issue_token(JTI, #{<<"sub">> => SubjectID, <<"email">> => SubjectEmail}, unlimited),
#token_keeper_AuthData{
id = undefined,
token = Token,
status = active,
context = Context,
metadata = #{
?META_USER_ID := SubjectID,
?META_USER_EMAIL := SubjectEmail,
?META_USER_REALM := <<"external">>
},
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) ->
Client = mk_client(C),
{ok, Token} = issue_dummy_token(C),
#token_keeper_InvalidToken{} =
(catch call_get_by_token(Token, ?TOKEN_SOURCE_CONTEXT(), Client)).
-spec no_token_claim_test(config()) -> ok.
no_token_claim_test(C) ->
Client = mk_client(C),
JTI = unique_id(),
SubjectID = <<"TEST">>,
{ok, Token} = issue_token(JTI, #{<<"sub">> => SubjectID}, unlimited),
#token_keeper_AuthDataNotFound{} =
(catch call_get_by_token(Token, ?TOKEN_SOURCE_CONTEXT(), Client)).
-spec bouncer_context_from_claims_test(config()) -> ok.
bouncer_context_from_claims_test(C) ->
Client = mk_client(C),
JTI = unique_id(),
SubjectID = <<"TEST">>,
{ok, Token} = issue_token_with_context(JTI, SubjectID),
#token_keeper_AuthData{
id = undefined,
token = Token,
status = active,
context = Context,
metadata = #{?META_PARTY_ID := 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 = #{?META_PARTY_ID := SubjectID, ?META_CAPI_CONSUMER := <<"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 = #{?META_PARTY_ID := 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)).
-spec basic_issuing_test(config()) -> ok.
basic_issuing_test(C) ->
Client = mk_client(C),
JTI = unique_id(),
BinaryContextFragment = create_bouncer_context(JTI),
Context = #bctx_ContextFragment{
type = v1_thrift_binary,
content = BinaryContextFragment
},
Metadata = #{<<"my">> => <<"metadata">>},
#token_keeper_AuthData{
id = undefined,
token = Token,
status = active,
context = Context,
metadata = Metadata,
authority = ?TK_AUTHORITY_CAPI
} = AuthData = call_create_ephemeral(Context, Metadata, Client),
ok = verify_token(Token, BinaryContextFragment, Metadata, JTI),
AuthData = call_get_by_token(Token, ?TOKEN_SOURCE_CONTEXT(), Client).
-spec jti_and_authority_blacklist_test(config()) -> ok.
jti_and_authority_blacklist_test(C) ->
Client = mk_client(C),
JTI = <<"MYCOOLKEY">>,
{ok, Token0} = issue_token(JTI, #{}, unlimited, primary),
#token_keeper_AuthDataRevoked{} =
(catch call_get_by_token(Token0, ?TOKEN_SOURCE_CONTEXT(), Client)),
{ok, Token1} = issue_token(JTI, #{}, unlimited, secondary),
#token_keeper_AuthDataNotFound{} =
(catch call_get_by_token(Token1, ?TOKEN_SOURCE_CONTEXT(), Client)).
-spec empty_blacklist_test(config()) -> ok.
empty_blacklist_test(C) ->
Client = mk_client(C),
JTI = <<"MYCOOLKEY">>,
{ok, Token1} = issue_token(JTI, #{}, unlimited, primary),
#token_keeper_AuthDataNotFound{} =
(catch call_get_by_token(Token1, ?TOKEN_SOURCE_CONTEXT(), Client)).
%%-------------------------------------
%% others test group
-spec simple_create_test(config()) -> ok.
simple_create_test(C) ->
Client = mk_client(C),
ID = unique_id(),
Metadata = #{<<"my">> => <<"metadata">>},
JTI = unique_id(),
Context = #bctx_ContextFragment{
type = v1_thrift_binary,
content = create_bouncer_context(JTI)
},
%% create
#token_keeper_AuthData{
id = ID,
status = active,
context = Context,
metadata = Metadata,
authority = ?TK_AUTHORITY_CAPI
} = call_create(ID, Context, Metadata, Client),
%% revoke
ok = call_revoke(ID, Client),
%% get
#token_keeper_AuthData{
id = ID,
status = revoked,
context = Context,
metadata = Metadata
} = call_get(ID, Client).
-spec create_twice_test(config()) -> ok.
create_twice_test(C) ->
Client = mk_client(C),
ID = unique_id(),
JTI = unique_id(),
Metadata = #{<<"my">> => <<"metadata">>},
Context = #bctx_ContextFragment{
type = v1_thrift_binary,
content = create_bouncer_context(JTI)
},
%% create: first time
#token_keeper_AuthData{
id = ID,
status = active,
context = _Context,
metadata = Metadata,
authority = ?TK_AUTHORITY_CAPI
} = call_create(ID, Context, Metadata, Client),
%% create: second time
#token_keeper_AuthDataAlreadyExists{} = (catch call_create(ID, Context, Metadata, Client)).
-spec revoke_twice_test(config()) -> ok.
revoke_twice_test(C) ->
Client = mk_client(C),
ID = unique_id(),
JTI = unique_id(),
Metadata = #{<<"my">> => <<"metadata">>},
Context = #bctx_ContextFragment{
type = v1_thrift_binary,
content = create_bouncer_context(JTI)
},
%% create
#token_keeper_AuthData{
id = ID,
status = active,
context = Context,
metadata = Metadata,
authority = ?TK_AUTHORITY_CAPI
} = call_create(ID, Context, Metadata, Client),
ok = call_revoke(ID, Client),
#token_keeper_AuthData{
id = ID,
status = revoked,
context = Context,
metadata = Metadata
} = call_get(ID, Client),
ok = call_revoke(ID, Client),
#token_keeper_AuthData{
id = ID,
status = revoked,
context = Context,
metadata = Metadata
} = call_get(ID, Client).
-spec revoke_notexisted_test(config()) -> ok.
revoke_notexisted_test(C) ->
#token_keeper_AuthDataNotFound{} = (catch call_revoke(unique_id(), mk_client(C))).
-spec get_notexisted_test(config()) -> ok.
get_notexisted_test(C) ->
#token_keeper_AuthDataNotFound{} = (catch call_get(unique_id(), mk_client(C))).
-spec getbytoken_test(config()) -> ok.
getbytoken_test(C) ->
Client = mk_client(C),
ID = unique_id(),
JTI = ID,
Metadata = #{<<"my">> => <<"metadata">>},
Context = #bctx_ContextFragment{
type = v1_thrift_binary,
content = create_bouncer_context(JTI)
},
%% create
#token_keeper_AuthData{
id = ID,
token = Token,
status = active,
context = Context,
metadata = Metadata,
authority = ?TK_AUTHORITY_CAPI
} = call_create(ID, Context, Metadata, Client),
%% getbytoken
#token_keeper_AuthData{
id = ID,
token = Token,
status = active,
context = Context,
metadata = Metadata,
authority = ?TK_AUTHORITY_CAPI
} = call_get_by_token(Token, ?TOKEN_SOURCE_CONTEXT(), Client).
%% internal
mk_client(C) ->
WoodyCtx = woody_context:new(genlib:to_binary(?CONFIG(testcase, C))),
ServiceURLs = ?CONFIG(service_urls, C),
{WoodyCtx, ServiceURLs}.
call_get_by_token(Token, TokenSourceContext, Client) ->
call_token_keeper('GetByToken', {Token, TokenSourceContext}, Client).
call_create_ephemeral(ContextFragment, Metadata, Client) ->
call_token_keeper('CreateEphemeral', {ContextFragment, Metadata}, Client).
call_get(ID, Client) ->
call_token_keeper('Get', {ID}, Client).
call_revoke(ID, Client) ->
call_token_keeper('Revoke', {ID}, Client).
call_create(ID, ContextFragment, Metadata, Client) ->
call_token_keeper('Create', {ID, ContextFragment, Metadata}, Client).
call_token_keeper(Operation, Args, Client) ->
call(token_keeper, Operation, Args, Client).
call(ServiceName, Fn, Args, {WoodyCtx, ServiceURLs}) ->
Service = get_service_spec(ServiceName),
Opts = #{
url => maps:get(ServiceName, ServiceURLs),
event_handler => scoper_woody_event_handler
},
case woody_client:call({Service, Fn, Args}, Opts, WoodyCtx) of
{ok, Response} ->
Response;
{exception, Exception} ->
throw(Exception)
end.
get_service_spec(token_keeper) ->
{tk_token_keeper_thrift, 'TokenKeeper'}.
%%
assert_context(TokenInfo, EncodedContextFragment) ->
#bctx_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({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).
assert_user({claim_token, _}, undefined) ->
ok;
assert_user({api_key_token, _, _}, undefined) ->
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).
%%
verify_token(Token, BinaryContextFragment, Metadata, _JTI) ->
EncodedContextFragment = base64:encode(BinaryContextFragment),
case tk_token_jwt:verify(Token, #{}) of
{ok, TokenInfo} ->
#{
%<<"jti">> := JTI, %% FIXME this will never match
<<"bouncer_ctx">> := #{
<<"ty">> := <<"v1_thrift_binary">>,
<<"ct">> := EncodedContextFragment
},
<<"tk_metadata">> := Metadata
} = tk_token_jwt:get_claims(TokenInfo),
ok;
Error ->
Error
end.
%%
make_auth_expiration(Timestamp) when is_integer(Timestamp) ->
genlib_rfc3339:format(Timestamp, second);
make_auth_expiration(unlimited) ->
undefined.
%%
create_bouncer_context(JTI) ->
Acc0 = bouncer_context_helpers:empty(),
Acc1 = bouncer_context_helpers:add_auth(
#{
method => <<"ClaimToken">>,
token => #{id => JTI}
},
Acc0
),
encode_context_fragment_content(Acc1).
issue_token(JTI, Claims0, Expiration) ->
issue_token(JTI, Claims0, Expiration, test).
issue_token(JTI, Claims0, Expiration, Issuer) ->
Claims1 = tk_token_jwt:create_claims(Claims0, Expiration),
tk_token_jwt:issue(JTI, Claims1, Issuer).
issue_token_with_context(JTI, SubjectID) ->
issue_token_with_context(JTI, SubjectID, #{}).
issue_token_with_context(JTI, SubjectID, AdditionalClaims) ->
FragmentContent = create_bouncer_context(JTI),
issue_token(
JTI,
AdditionalClaims#{
<<"sub">> => SubjectID,
<<"bouncer_ctx">> => #{
<<"ty">> => <<"v1_thrift_binary">>,
<<"ct">> => base64:encode(FragmentContent)
}
},
unlimited
).
issue_dummy_token(Config) ->
Claims = #{
<<"jti">> => unique_id(),
<<"sub">> => <<"TEST">>,
<<"exp">> => 0
},
BadPemFile = get_filename("keys/local/dummy.pem", Config),
BadJWK = jose_jwk:from_pem_file(BadPemFile),
GoodPemFile = get_filename("keys/local/private.pem", Config),
GoodJWK = jose_jwk:from_pem_file(GoodPemFile),
JWKPublic = jose_jwk:to_public(GoodJWK),
{_Module, PublicKey} = JWKPublic#jose_jwk.kty,
{_PemEntry, Data, _} = public_key:pem_entry_encode('SubjectPublicKeyInfo', PublicKey),
KID = jose_base64url:encode(crypto:hash(sha256, Data)),
JWT = jose_jwt:sign(BadJWK, #{<<"alg">> => <<"RS256">>, <<"kid">> => KID}, Claims),
{_Modules, Token} = jose_jws:compact(JWT),
{ok, Token}.
get_filename(Key, Config) ->
filename:join(?config(data_dir, Config), Key).
unique_id() ->
<<ID:64>> = snowflake:new(),
genlib_format:format_int_base(ID, 62).
decode_bouncer_fragment(#bctx_ContextFragment{type = v1_thrift_binary, content = Content}) ->
Type = {struct, struct, {bouncer_context_v1_thrift, 'ContextFragment'}},
Codec = thrift_strict_binary_codec:new(Content),
{ok, Fragment, _} = thrift_strict_binary_codec:read(Codec, Type),
Fragment.
encode_context_fragment_content(ContextFragment) ->
Type = {struct, struct, {bouncer_context_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.

View File

@ -1 +0,0 @@
entries: []

View File

@ -1,113 +0,0 @@
-module(tk_token_jwt_tests_SUITE).
-include_lib("stdlib/include/assert.hrl").
-include_lib("common_test/include/ct.hrl").
-include_lib("jose/include/jose_jwk.hrl").
-export([all/0]).
-export([init_per_suite/1]).
-export([end_per_suite/1]).
-export([
verify_test/1,
bad_token_test/1,
bad_signee_test/1
]).
-type test_case_name() :: atom().
-type config() :: [{atom(), any()}].
-spec all() -> [test_case_name()].
all() ->
[
verify_test,
bad_token_test,
bad_signee_test
].
-spec init_per_suite(config()) -> config().
init_per_suite(Config) ->
Apps =
genlib_app:start_application(woody) ++
genlib_app:start_application_with(scoper, [
{storage, scoper_storage_logger}
]) ++
genlib_app:start_application_with(
token_keeper,
[
{ip, "127.0.0.1"},
{port, 8022},
{services, #{
token_keeper => #{
path => <<"/v1/token-keeper">>
}
}},
{jwt, #{
keyset => #{
test => #{
source => {pem_file, get_keysource("keys/local/private.pem", Config)},
authority => test
}
}
}}
]
),
[{apps, Apps}] ++ Config.
-spec end_per_suite(config()) -> _.
end_per_suite(Config) ->
Config.
%%
-spec verify_test(config()) -> _.
verify_test(_) ->
JTI = unique_id(),
PartyID = <<"TEST">>,
{ok, Token} = issue_token(JTI, #{<<"sub">> => PartyID, <<"TEST">> => <<"TEST">>}, unlimited),
{ok, {#{<<"jti">> := JTI, <<"sub">> := PartyID, <<"TEST">> := <<"TEST">>}, test, #{}}} = tk_token_jwt:verify(
Token,
#{}
).
-spec bad_token_test(config()) -> _.
bad_token_test(Config) ->
{ok, Token} = issue_dummy_token(Config),
{error, invalid_signature} = tk_token_jwt:verify(Token, #{}).
-spec bad_signee_test(config()) -> _.
bad_signee_test(_) ->
Claims = tk_token_jwt:create_claims(#{}, unlimited),
{error, nonexistent_key} =
tk_token_jwt:issue(unique_id(), Claims, random).
%%
issue_token(JTI, Claims0, Expiration) ->
Claims1 = tk_token_jwt:create_claims(Claims0, Expiration),
tk_token_jwt:issue(JTI, Claims1, test).
issue_dummy_token(Config) ->
Claims = #{
<<"jti">> => unique_id(),
<<"sub">> => <<"TEST">>,
<<"exp">> => 0
},
BadPemFile = get_keysource("keys/local/dummy.pem", Config),
BadJWK = jose_jwk:from_pem_file(BadPemFile),
GoodPemFile = get_keysource("keys/local/private.pem", Config),
GoodJWK = jose_jwk:from_pem_file(GoodPemFile),
JWKPublic = jose_jwk:to_public(GoodJWK),
{_Module, PublicKey} = JWKPublic#jose_jwk.kty,
{_PemEntry, Data, _} = public_key:pem_entry_encode('SubjectPublicKeyInfo', PublicKey),
KID = jose_base64url:encode(crypto:hash(sha256, Data)),
JWT = jose_jwt:sign(BadJWK, #{<<"alg">> => <<"RS256">>, <<"kid">> => KID}, Claims),
{_Modules, Token} = jose_jws:compact(JWT),
{ok, Token}.
get_keysource(Key, Config) ->
filename:join(?config(data_dir, Config), Key).
unique_id() ->
<<ID:64>> = snowflake:new(),
genlib_format:format_int_base(ID, 62).

View File

@ -1,13 +0,0 @@
-----BEGIN RSA PRIVATE KEY-----
MIICXAIBAAKBgQCqGKukO1De7zhZj6+H0qtjTkVxwTCpvKe4eCZ0FPqri0cb2JZfXJ/DgYSF6vUp
wmJG8wVQZKjeGcjDOL5UlsuusFncCzWBQ7RKNUSesmQRMSGkVb1/3j+skZ6UtW+5u09lHNsj6tQ5
1s1SPrCBkedbNf0Tp0GbMJDyR4e9T04ZZwIDAQABAoGAFijko56+qGyN8M0RVyaRAXz++xTqHBLh
3tx4VgMtrQ+WEgCjhoTwo23KMBAuJGSYnRmoBZM3lMfTKevIkAidPExvYCdm5dYq3XToLkkLv5L2
pIIVOFMDG+KESnAFV7l2c+cnzRMW0+b6f8mR1CJzZuxVLL6Q02fvLi55/mbSYxECQQDeAw6fiIQX
GukBI4eMZZt4nscy2o12KyYner3VpoeE+Np2q+Z3pvAMd/aNzQ/W9WaI+NRfcxUJrmfPwIGm63il
AkEAxCL5HQb2bQr4ByorcMWm/hEP2MZzROV73yF41hPsRC9m66KrheO9HPTJuo3/9s5p+sqGxOlF
L0NDt4SkosjgGwJAFklyR1uZ/wPJjj611cdBcztlPdqoxssQGnh85BzCj/u3WqBpE2vjvyyvyI5k
X6zk7S0ljKtt2jny2+00VsBerQJBAJGC1Mg5Oydo5NwD6BiROrPxGo2bpTbu/fhrT8ebHkTz2epl
U9VQQSQzY1oZMVX8i1m5WUTLPz2yLJIBQVdXqhMCQBGoiuSoSjafUhV7i1cEGpb88h5NBYZzWXGZ
37sJ5QsW+sJyoNde3xH8vdXhzU7eT82D6X/scw9RZz+/6rCJ4p0=
-----END RSA PRIVATE KEY-----

View File

@ -1,9 +0,0 @@
-----BEGIN RSA PRIVATE KEY-----
MIIBOwIBAAJBAK9fx7qOJT7Aoseu7KKgaLagBh3wvDzg7F/ZMtGbPFikJnnvRWvF
B5oEGbMPblvtF0/fjqfu+eqjP3Z1tUSn7TkCAwEAAQJABUY5KIgr4JZEjwLYxQ9T
9uIbLP1Xe/E7yqoqmBk2GGhSrPY0OeRkYnUVLcP96UPQhF63iuG8VF6uZ7oAPsq+
gQIhANZy3jSCzPjXYHRU1kRqQzpt2S+OqoEiqQ6YG1HrC/VxAiEA0Vq6JlQK2tOX
37SS00dK0Qog4Qi8dN73GliFQNP18EkCIQC4epSA48zkfJMzQBAbRraSuxDNApPX
BzQbo+pMrEDbYQIgY4AncQgIkLB4Qk5kah48JNYXglzQlQtTjiX8Ty9ueGECIQCM
GD3UbQKiA0gf5plBA24I4wFVKxxa4wXbW/7SfP6XmQ==
-----END RSA PRIVATE KEY-----

View File

@ -1,4 +0,0 @@
-----BEGIN PUBLIC KEY-----
MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAK9fx7qOJT7Aoseu7KKgaLagBh3wvDzg
7F/ZMtGbPFikJnnvRWvFB5oEGbMPblvtF0/fjqfu+eqjP3Z1tUSn7TkCAwEAAQ==
-----END PUBLIC KEY-----

971
test/token_keeper_SUITE.erl Normal file
View File

@ -0,0 +1,971 @@
-module(token_keeper_SUITE).
-include_lib("common_test/include/ct.hrl").
-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_base_thrift.hrl").
-include_lib("bouncer_proto/include/bouncer_context_v1_thrift.hrl").
-export([all/0]).
-export([groups/0]).
-export([init_per_suite/1]).
-export([end_per_suite/1]).
-export([init_per_group/2]).
-export([end_per_group/2]).
-export([init_per_testcase/2]).
-export([end_per_testcase/2]).
-export([authenticate_invalid_token_type_fail/1]).
-export([authenticate_invalid_token_key_fail/1]).
-export([authenticate_phony_api_key_token_ok/1]).
-export([authenticate_user_session_token_ok/1]).
-export([authenticate_invoice_template_access_token_ok/1]).
-export([authenticate_invoice_template_access_token_no_access/1]).
-export([authenticate_invoice_template_access_token_invalid_access/1]).
-export([authenticate_claim_token_no_context_fail/1]).
-export([authenticate_legacy_claim_token_ok/1]).
-export([authenticate_blacklisted_jti_fail/1]).
-export([authenticate_non_blacklisted_jti_ok/1]).
-export([authenticate_ephemeral_claim_token_ok/1]).
-export([issue_ephemeral_token_ok/1]).
-export([authenticate_offline_token_not_found_fail/1]).
-export([authenticate_offline_token_revoked_fail/1]).
-export([authenticate_offline_token_ok/1]).
-export([issue_offline_token_ok/1]).
-export([issue_duplicate_offline_token_fail/1]).
-export([get_authdata_by_id_ok/1]).
-export([get_authdata_by_id_not_found_fail/1]).
-export([revoke_authdata_by_id_ok/1]).
-export([revoke_authdata_by_id_not_found_fail/1]).
-type config() :: ct_helper:config().
-type group_name() :: atom().
-type test_case_name() :: atom().
-define(CONFIG(Key, C), (element(2, lists:keyfind(Key, 1, C)))).
%%
-define(TOKEN_SOURCE_CONTEXT, ?TOKEN_SOURCE_CONTEXT(<<"http://spanish.inquisition">>)).
-define(TOKEN_SOURCE_CONTEXT(SourceURL), #token_keeper_TokenSourceContext{request_origin = SourceURL}).
-define(USER_TOKEN_SOURCE, <<"https://dashboard.rbk.money">>).
-define(META_PARTY_ID, <<"test.rbkmoney.party.id">>).
-define(META_USER_ID, <<"test.rbkmoney.user.id">>).
-define(META_USER_EMAIL, <<"test.rbkmoney.user.email">>).
-define(META_USER_REALM, <<"test.rbkmoney.user.realm">>).
-define(META_CAPI_CONSUMER, <<"test.rbkmoney.capi.consumer">>).
-define(TK_AUTHORITY_KEYCLOAK, <<"test.rbkmoney.keycloak">>).
-define(TK_AUTHORITY_APIKEYMGMT, <<"test.rbkmoney.apikeymgmt">>).
-define(TK_AUTHORITY_CAPI, <<"test.rbkmoney.capi">>).
-define(TK_RESOURCE_DOMAIN, <<"test-domain">>).
%%
-spec all() -> [atom()].
all() ->
[
{group, external_detect_token},
{group, external_invoice_template_access_token},
{group, external_legacy_claim},
{group, blacklist},
{group, ephemeral},
{group, offline}
].
-spec groups() -> [{group_name(), list(), [test_case_name()]}].
groups() ->
[
{external_detect_token, [parallel], [
authenticate_invalid_token_type_fail,
authenticate_invalid_token_key_fail,
authenticate_phony_api_key_token_ok,
authenticate_user_session_token_ok
]},
{external_invoice_template_access_token, [parallel], [
authenticate_invalid_token_type_fail,
authenticate_invalid_token_key_fail,
authenticate_invoice_template_access_token_ok,
authenticate_invoice_template_access_token_no_access,
authenticate_invoice_template_access_token_invalid_access
]},
{external_legacy_claim, [parallel], [
authenticate_claim_token_no_context_fail,
authenticate_legacy_claim_token_ok
]},
{ephemeral, [parallel], [
authenticate_claim_token_no_context_fail,
authenticate_ephemeral_claim_token_ok,
issue_ephemeral_token_ok
]},
{offline, [parallel], [
authenticate_offline_token_not_found_fail,
authenticate_offline_token_revoked_fail,
authenticate_offline_token_ok,
issue_offline_token_ok,
issue_duplicate_offline_token_fail,
get_authdata_by_id_ok,
get_authdata_by_id_not_found_fail,
revoke_authdata_by_id_ok,
revoke_authdata_by_id_not_found_fail
]},
{blacklist, [parallel], [
authenticate_blacklisted_jti_fail,
authenticate_non_blacklisted_jti_ok
]}
].
-spec init_per_suite(config()) -> config().
init_per_suite(C) ->
Apps =
genlib_app:start_application(woody) ++
genlib_app:start_application_with(scoper, [
{storage, scoper_storage_logger}
]),
[{suite_apps, Apps} | C].
-spec end_per_suite(config()) -> ok.
end_per_suite(C) ->
genlib_app:stop_unload_applications(?CONFIG(suite_apps, C)).
%% @TODO Pending configurator
-spec init_per_group(group_name(), config()) -> config().
init_per_group(external_detect_token = Name, C) ->
C0 = start_keeper([
{authenticator, #{
service => #{
path => <<"/v2/authenticator">>
},
authorities => #{
?TK_AUTHORITY_KEYCLOAK =>
#{
sources => [extract_method_detect_token()]
}
}
}},
{tokens, #{
jwt => #{
authority_bindings => #{
?TK_AUTHORITY_KEYCLOAK => ?TK_AUTHORITY_KEYCLOAK
},
keyset => #{
?TK_AUTHORITY_KEYCLOAK => #{
source => {pem_file, get_filename("keys/local/public.pem", C)}
}
}
}
}}
]),
ServiceUrls = #{
token_authenticator => mk_url(<<"/v2/authenticator">>)
},
[{groupname, Name}, {service_urls, ServiceUrls} | C0 ++ C];
init_per_group(external_invoice_template_access_token = Name, C) ->
C0 = start_keeper([
{authenticator, #{
service => #{
path => <<"/v2/authenticator">>
},
authorities => #{
?TK_AUTHORITY_CAPI =>
#{
sources => [extract_method_invoice_tpl_token()]
}
}
}},
{tokens, #{
jwt => #{
authority_bindings => #{
?TK_AUTHORITY_CAPI => ?TK_AUTHORITY_CAPI
},
keyset => #{
?TK_AUTHORITY_CAPI => #{
source => {pem_file, get_filename("keys/local/public.pem", C)}
}
}
}
}}
]),
ServiceUrls = #{
token_authenticator => mk_url(<<"/v2/authenticator">>)
},
[{groupname, Name}, {service_urls, ServiceUrls} | C0 ++ C];
init_per_group(external_legacy_claim = Name, C) ->
C0 = start_keeper([
{authenticator, #{
service => #{
path => <<"/v2/authenticator">>
},
authorities => #{
?TK_AUTHORITY_CAPI =>
#{
sources => [
{legacy_claim, #{
metadata_mappings => #{
party_id => ?META_PARTY_ID,
consumer => ?META_CAPI_CONSUMER
}
}}
]
}
}
}},
{tokens, #{
jwt => #{
authority_bindings => #{
?TK_AUTHORITY_CAPI => ?TK_AUTHORITY_CAPI
},
keyset => #{
?TK_AUTHORITY_CAPI => #{
source => {pem_file, get_filename("keys/local/public.pem", C)}
}
}
}
}}
]),
ServiceUrls = #{
token_authenticator => mk_url(<<"/v2/authenticator">>)
},
[{groupname, Name}, {service_urls, ServiceUrls} | C0 ++ C];
init_per_group(blacklist = Name, C) ->
C0 = start_keeper(
[
{authenticator, #{
service => #{
path => <<"/v2/authenticator">>
},
authorities => #{
<<"blacklisting_authority">> =>
#{
sources => [extract_method_detect_token()]
},
?TK_AUTHORITY_CAPI =>
#{
sources => [extract_method_detect_token()]
}
}
}},
{tokens, #{
jwt => #{
authority_bindings => #{
<<"blacklisting_authority">> => <<"blacklisting_authority">>,
?TK_AUTHORITY_CAPI => ?TK_AUTHORITY_CAPI
},
keyset => #{
<<"blacklisting_authority">> => #{
source => {pem_file, get_filename("keys/local/private.pem", C)}
},
?TK_AUTHORITY_CAPI => #{
source => {pem_file, get_filename("keys/secondary/private.pem", C)}
}
}
}
}}
],
get_filename("blacklisted_keys.yaml", C)
),
ServiceUrls = #{
token_authenticator => mk_url(<<"/v2/authenticator">>)
},
[{groupname, Name}, {service_urls, ServiceUrls} | C0 ++ C];
init_per_group(ephemeral = Name, C) ->
C0 = start_keeper([
{authenticator, #{
service => #{
path => <<"/v2/authenticator">>
},
authorities => #{
?TK_AUTHORITY_CAPI => #{
sources => [
{claim, #{}}
]
}
}
}},
{authorities, #{
?TK_AUTHORITY_CAPI =>
#{
service => #{
path => <<"/v2/authority/com.rbkmoney.access.capi">>
},
type =>
{ephemeral, #{
token => #{
type => jwt
}
}}
}
}},
{tokens, #{
jwt => #{
authority_bindings => #{
?TK_AUTHORITY_CAPI => ?TK_AUTHORITY_CAPI
},
keyset => #{
?TK_AUTHORITY_CAPI => #{
source => {pem_file, get_filename("keys/local/private.pem", C)}
}
}
}
}}
]),
ServiceUrls = #{
token_authenticator => mk_url(<<"/v2/authenticator">>),
{token_ephemeral_authority, ?TK_AUTHORITY_CAPI} => mk_url(<<"/v2/authority/com.rbkmoney.access.capi">>)
},
[{groupname, Name}, {service_urls, ServiceUrls} | C0 ++ C];
init_per_group(offline = Name, C) ->
C0 = start_keeper([
{authenticator, #{
service => #{
path => <<"/v2/authenticator">>
},
authorities => #{
?TK_AUTHORITY_APIKEYMGMT =>
#{
sources => [
{storage, #{
name => ?TK_AUTHORITY_APIKEYMGMT
}}
]
}
}
}},
{authorities, #{
?TK_AUTHORITY_APIKEYMGMT =>
#{
service => #{
path => <<"/v2/authority/com.rbkmoney.apikemgmt">>
},
type =>
{offline, #{
token => #{
type => jwt
},
storage => #{
name => ?TK_AUTHORITY_APIKEYMGMT
}
}}
}
}},
{tokens, #{
jwt => #{
authority_bindings => #{
?TK_AUTHORITY_APIKEYMGMT => ?TK_AUTHORITY_APIKEYMGMT
},
keyset => #{
?TK_AUTHORITY_APIKEYMGMT => #{
source => {pem_file, get_filename("keys/local/private.pem", C)}
}
}
}
}},
{storages, #{
?TK_AUTHORITY_APIKEYMGMT =>
{machinegun, #{
namespace => apikeymgmt,
automaton => #{
url => <<"http://machinegun:8022/v1/automaton">>,
event_handler => [scoper_woody_event_handler],
transport_opts => #{}
}
}}
}}
]),
ServiceUrls = #{
token_authenticator => mk_url(<<"/v2/authenticator">>),
{token_authority, ?TK_AUTHORITY_APIKEYMGMT} => mk_url(<<"/v2/authority/com.rbkmoney.apikemgmt">>)
},
[{groupname, Name}, {service_urls, ServiceUrls} | C0 ++ C].
-spec end_per_group(group_name(), config()) -> _.
end_per_group(_GroupName, C) ->
ok = stop_keeper(C),
ok.
-spec init_per_testcase(atom(), config()) -> config().
init_per_testcase(Name, C) ->
[{testcase, Name} | C].
-spec end_per_testcase(atom(), config()) -> config().
end_per_testcase(_Name, _C) ->
ok.
%%
-spec authenticate_invalid_token_type_fail(config()) -> _.
authenticate_invalid_token_type_fail(C) ->
Token = <<"BLAH">>,
?assertThrow(#token_keeper_InvalidToken{}, call_authenticate(Token, ?TOKEN_SOURCE_CONTEXT, C)).
-spec authenticate_invalid_token_key_fail(config()) -> _.
authenticate_invalid_token_key_fail(C) ->
Token = issue_dummy_token(C),
?assertThrow(#token_keeper_InvalidToken{}, call_authenticate(Token, ?TOKEN_SOURCE_CONTEXT, C)).
-spec authenticate_phony_api_key_token_ok(config()) -> _.
authenticate_phony_api_key_token_ok(C) ->
JTI = unique_id(),
SubjectID = unique_id(),
Claims = get_phony_api_key_claims(JTI, SubjectID),
Token = issue_token(Claims, C),
#token_keeper_AuthData{
id = undefined,
token = Token,
status = active,
context = Context,
metadata = #{?META_PARTY_ID := SubjectID},
authority = ?TK_AUTHORITY_KEYCLOAK
} = call_authenticate(Token, ?TOKEN_SOURCE_CONTEXT, C),
_ = assert_context({api_key_token, JTI, SubjectID}, Context).
-spec authenticate_user_session_token_ok(config()) -> _.
authenticate_user_session_token_ok(C) ->
JTI = unique_id(),
SubjectID = unique_id(),
SubjectEmail = <<"test@test.test">>,
Claims = get_user_session_token_claims(JTI, SubjectID, SubjectEmail),
Token = issue_token(Claims, C),
#token_keeper_AuthData{
id = undefined,
token = Token,
status = active,
context = Context,
metadata = #{
?META_USER_ID := SubjectID,
?META_USER_EMAIL := SubjectEmail,
?META_USER_REALM := <<"external">>
},
authority = ?TK_AUTHORITY_KEYCLOAK
} = call_authenticate(Token, ?TOKEN_SOURCE_CONTEXT(?USER_TOKEN_SOURCE), C),
_ = assert_context({user_session_token, JTI, SubjectID, SubjectEmail, unlimited}, Context).
-spec authenticate_invoice_template_access_token_ok(config()) -> _.
authenticate_invoice_template_access_token_ok(C) ->
JTI = unique_id(),
InvoiceTemplateID = unique_id(),
SubjectID = unique_id(),
Claims = get_invoice_access_template_token_claims(JTI, SubjectID, InvoiceTemplateID),
Token = issue_token(Claims, C),
#token_keeper_AuthData{
id = undefined,
token = Token,
status = active,
context = Context,
metadata = #{?META_PARTY_ID := SubjectID},
authority = ?TK_AUTHORITY_CAPI
} = call_authenticate(Token, ?TOKEN_SOURCE_CONTEXT, C),
_ = assert_context({invoice_template_access_token, JTI, SubjectID, InvoiceTemplateID}, Context).
-spec authenticate_invoice_template_access_token_no_access(config()) -> _.
authenticate_invoice_template_access_token_no_access(C) ->
JTI = unique_id(),
SubjectID = unique_id(),
Claims = get_resource_access_claims(JTI, SubjectID, #{}),
Token = issue_token(Claims, C),
?assertThrow(#token_keeper_AuthDataNotFound{}, call_authenticate(Token, ?TOKEN_SOURCE_CONTEXT, C)).
-spec authenticate_invoice_template_access_token_invalid_access(config()) -> _.
authenticate_invoice_template_access_token_invalid_access(C) ->
JTI = unique_id(),
InvoiceID = unique_id(),
SubjectID = unique_id(),
Claims = get_resource_access_claims(JTI, SubjectID, #{
?TK_RESOURCE_DOMAIN => #{
<<"roles">> => [
<<"invoices.", InvoiceID/binary, ":read">>
]
}
}),
Token = issue_token(Claims, C),
?assertThrow(#token_keeper_AuthDataNotFound{}, call_authenticate(Token, ?TOKEN_SOURCE_CONTEXT, C)).
-spec authenticate_blacklisted_jti_fail(config()) -> _.
authenticate_blacklisted_jti_fail(C) ->
JTI = <<"MYCOOLKEY">>,
SubjectID = unique_id(),
Claims = get_phony_api_key_claims(JTI, SubjectID),
Token = issue_token_with(Claims, get_filename("keys/local/private.pem", C)),
?assertThrow(#token_keeper_AuthDataRevoked{}, call_authenticate(Token, ?TOKEN_SOURCE_CONTEXT, C)).
-spec authenticate_non_blacklisted_jti_ok(config()) -> _.
authenticate_non_blacklisted_jti_ok(C) ->
JTI = <<"MYCOOLKEY">>,
SubjectID = unique_id(),
Claims = get_phony_api_key_claims(JTI, SubjectID),
Token = issue_token_with(Claims, get_filename("keys/secondary/private.pem", C)),
?assertMatch(#token_keeper_AuthData{}, call_authenticate(Token, ?TOKEN_SOURCE_CONTEXT, C)).
-spec authenticate_claim_token_no_context_fail(config()) -> _.
authenticate_claim_token_no_context_fail(C) ->
JTI = unique_id(),
SubjectID = unique_id(),
Claims = get_base_claims(JTI, SubjectID),
Token = issue_token(Claims, C),
?assertThrow(#token_keeper_AuthDataNotFound{}, call_authenticate(Token, ?TOKEN_SOURCE_CONTEXT, C)).
-spec authenticate_legacy_claim_token_ok(config()) -> _.
authenticate_legacy_claim_token_ok(C) ->
JTI = unique_id(),
SubjectID = unique_id(),
ContextFragment = create_encoded_bouncer_context(JTI),
Consumer = <<"client">>,
Claims = get_claim_token_claims(JTI, SubjectID, ContextFragment, undefined, Consumer),
Token = issue_token(Claims, C),
#token_keeper_AuthData{
id = undefined,
token = Token,
status = active,
context = Context,
metadata = #{?META_PARTY_ID := SubjectID, ?META_CAPI_CONSUMER := Consumer},
authority = ?TK_AUTHORITY_CAPI
} = call_authenticate(Token, ?TOKEN_SOURCE_CONTEXT, C),
_ = assert_context({claim_token, JTI}, Context).
-spec authenticate_ephemeral_claim_token_ok(config()) -> _.
authenticate_ephemeral_claim_token_ok(C) ->
JTI = unique_id(),
ContextFragment = create_encoded_bouncer_context(JTI),
Metadata = #{<<"my metadata">> => <<"is here">>},
AuthorityID = ?TK_AUTHORITY_CAPI,
#token_keeper_AuthData{
id = undefined,
token = Token,
status = active,
context = Context,
metadata = Metadata
} = call_create_ephemeral(AuthorityID, ContextFragment, Metadata, C),
#token_keeper_AuthData{
id = undefined,
token = Token,
status = active,
context = Context,
metadata = Metadata,
authority = AuthorityID
} = call_authenticate(Token, ?TOKEN_SOURCE_CONTEXT, C),
_ = assert_context({claim_token, JTI}, Context).
-spec issue_ephemeral_token_ok(config()) -> _.
issue_ephemeral_token_ok(C) ->
JTI = unique_id(),
ContextFragment = create_encoded_bouncer_context(JTI),
Metadata = #{<<"my metadata">> => <<"is here">>},
AuthorityID = ?TK_AUTHORITY_CAPI,
#token_keeper_AuthData{
id = undefined,
status = active,
metadata = Metadata
} = call_create_ephemeral(AuthorityID, ContextFragment, Metadata, C).
-spec authenticate_offline_token_not_found_fail(config()) -> _.
authenticate_offline_token_not_found_fail(C) ->
JTI = unique_id(),
SubjectID = unique_id(),
Claims = get_base_claims(JTI, SubjectID),
Token = issue_token(Claims, C),
?assertThrow(#token_keeper_AuthDataNotFound{}, call_authenticate(Token, ?TOKEN_SOURCE_CONTEXT, C)).
-spec authenticate_offline_token_revoked_fail(config()) -> _.
authenticate_offline_token_revoked_fail(C) ->
JTI = unique_id(),
ContextFragment = create_encoded_bouncer_context(JTI),
Metadata = #{<<"my metadata">> => <<"is here">>},
AuthorityID = ?TK_AUTHORITY_APIKEYMGMT,
#token_keeper_AuthData{
id = JTI,
token = Token,
status = active
} = call_create(AuthorityID, JTI, ContextFragment, Metadata, C),
ok = call_revoke(AuthorityID, JTI, C),
?assertThrow(#token_keeper_AuthDataRevoked{}, call_authenticate(Token, ?TOKEN_SOURCE_CONTEXT, C)).
-spec authenticate_offline_token_ok(config()) -> _.
authenticate_offline_token_ok(C) ->
JTI = unique_id(),
ContextFragment = create_encoded_bouncer_context(JTI),
Metadata = #{<<"my metadata">> => <<"is here">>},
AuthorityID = ?TK_AUTHORITY_APIKEYMGMT,
#token_keeper_AuthData{
id = JTI,
token = Token,
status = active,
context = Context,
metadata = Metadata
} = call_create(AuthorityID, JTI, ContextFragment, Metadata, C),
#token_keeper_AuthData{
id = JTI,
token = Token,
status = active,
context = Context,
metadata = Metadata,
authority = AuthorityID
} = call_authenticate(Token, ?TOKEN_SOURCE_CONTEXT, C),
_ = assert_context({claim_token, JTI}, Context).
-spec issue_offline_token_ok(config()) -> _.
issue_offline_token_ok(C) ->
JTI = unique_id(),
ContextFragment = create_encoded_bouncer_context(JTI),
Metadata = #{<<"my metadata">> => <<"is here">>},
AuthorityID = ?TK_AUTHORITY_APIKEYMGMT,
#token_keeper_AuthData{
id = JTI,
status = active
} = call_create(AuthorityID, JTI, ContextFragment, Metadata, C).
-spec issue_duplicate_offline_token_fail(config()) -> _.
issue_duplicate_offline_token_fail(C) ->
JTI = unique_id(),
ContextFragment = create_encoded_bouncer_context(JTI),
Metadata = #{},
AuthorityID = ?TK_AUTHORITY_APIKEYMGMT,
#token_keeper_AuthData{
id = JTI,
status = active
} = call_create(AuthorityID, JTI, ContextFragment, Metadata, C),
?assertThrow(
#token_keeper_AuthDataAlreadyExists{},
call_create(AuthorityID, JTI, ContextFragment, Metadata, C)
).
-spec get_authdata_by_id_ok(config()) -> _.
get_authdata_by_id_ok(C) ->
JTI = unique_id(),
ContextFragment = create_encoded_bouncer_context(JTI),
Metadata = #{<<"my metadata">> => <<"is here">>},
AuthorityID = ?TK_AUTHORITY_APIKEYMGMT,
#token_keeper_AuthData{
id = JTI,
token = _,
status = active,
context = Context,
metadata = Metadata
} = call_create(AuthorityID, JTI, ContextFragment, Metadata, C),
#token_keeper_AuthData{
id = JTI,
token = undefined,
status = active,
context = Context,
metadata = Metadata
} = call_get(AuthorityID, JTI, C).
-spec get_authdata_by_id_not_found_fail(config()) -> _.
get_authdata_by_id_not_found_fail(C) ->
JTI = unique_id(),
AuthorityID = ?TK_AUTHORITY_APIKEYMGMT,
?assertThrow(#token_keeper_AuthDataNotFound{}, call_get(AuthorityID, JTI, C)).
-spec revoke_authdata_by_id_ok(config()) -> _.
revoke_authdata_by_id_ok(C) ->
JTI = unique_id(),
ContextFragment = create_encoded_bouncer_context(JTI),
Metadata = #{},
AuthorityID = ?TK_AUTHORITY_APIKEYMGMT,
#token_keeper_AuthData{
id = JTI,
status = active
} = call_create(AuthorityID, JTI, ContextFragment, Metadata, C),
ok = call_revoke(AuthorityID, JTI, C),
#token_keeper_AuthData{
id = JTI,
status = revoked
} = RevokedAuthData = call_get(AuthorityID, JTI, C),
ok = call_revoke(AuthorityID, JTI, C),
?assertEqual(RevokedAuthData, call_get(AuthorityID, JTI, C)).
-spec revoke_authdata_by_id_not_found_fail(config()) -> _.
revoke_authdata_by_id_not_found_fail(C) ->
JTI = unique_id(),
AuthorityID = ?TK_AUTHORITY_APIKEYMGMT,
?assertThrow(#token_keeper_AuthDataNotFound{}, call_revoke(AuthorityID, JTI, C)).
%%
get_base_claims(JTI, SubjectID) ->
#{
<<"jti">> => JTI,
<<"sub">> => SubjectID,
<<"exp">> => 0
}.
get_phony_api_key_claims(JTI, SubjectID) ->
get_base_claims(JTI, SubjectID).
get_user_session_token_claims(JTI, SubjectID, SubjectEmail) ->
maps:merge(#{<<"email">> => SubjectEmail}, get_base_claims(JTI, SubjectID)).
get_resource_access_claims(JTI, SubjectID, ResourceAccess) ->
maps:merge(#{<<"resource_access">> => ResourceAccess}, get_base_claims(JTI, SubjectID)).
get_invoice_access_template_token_claims(JTI, SubjectID, InvoiceTemplateID) ->
get_resource_access_claims(
JTI,
SubjectID,
#{
?TK_RESOURCE_DOMAIN => #{
<<"roles">> => [
<<"party.*.invoice_templates.", InvoiceTemplateID/binary, ".invoice_template_invoices:write">>,
<<"party.*.invoice_templates.", InvoiceTemplateID/binary, ":read">>
]
}
}
).
create_bouncer_context(JTI) ->
bouncer_context_helpers:add_auth(
#{
method => <<"ClaimToken">>,
token => #{id => JTI}
},
bouncer_context_helpers:empty()
).
create_encoded_bouncer_context(JTI) ->
Fragment = create_bouncer_context(JTI),
#bctx_ContextFragment{
type = v1_thrift_binary,
content = encode_context_fragment_content(Fragment)
}.
get_claim_token_claims(JTI, SubjectID, #bctx_ContextFragment{content = FragmentContent}, Metadata, Consumer) ->
genlib_map:compact(#{
<<"jti">> => JTI,
<<"sub">> => SubjectID,
<<"bouncer_ctx">> => #{
<<"ty">> => <<"v1_thrift_binary">>,
<<"ct">> => base64:encode(FragmentContent)
},
<<"tk_metadata">> => Metadata,
<<"cons">> => Consumer,
<<"exp">> => 0
}).
%%
mk_client(C) ->
WoodyCtx = woody_context:new(genlib:to_binary(?CONFIG(testcase, C))),
ServiceURLs = ?CONFIG(service_urls, C),
{WoodyCtx, ServiceURLs}.
call_authenticate(Token, TokenSourceContext, C) ->
call_token_authenticator('Authenticate', {Token, TokenSourceContext}, C).
call_create_ephemeral(AuthorityID, Context, Metadata, C) ->
call_token_ephemeral_authority(AuthorityID, 'Create', {Context, Metadata}, C).
call_create(AuthorityID, ID, Context, Metadata, C) ->
call_token_authority(AuthorityID, 'Create', {ID, Context, Metadata}, C).
call_get(AuthorityID, ID, C) ->
call_token_authority(AuthorityID, 'Get', {ID}, C).
call_revoke(AuthorityID, ID, C) ->
call_token_authority(AuthorityID, 'Revoke', {ID}, C).
call_token_authenticator(Operation, Args, C) ->
call(token_authenticator, Operation, Args, mk_client(C)).
call_token_authority(ID, Operation, Args, C) ->
call({token_authority, ID}, Operation, Args, mk_client(C)).
call_token_ephemeral_authority(ID, Operation, Args, C) ->
call({token_ephemeral_authority, ID}, Operation, Args, mk_client(C)).
call(ServiceName, Fn, Args, {WoodyCtx, ServiceURLs}) ->
Service = get_service_spec(ServiceName),
Opts = #{
url => maps:get(ServiceName, ServiceURLs),
event_handler => scoper_woody_event_handler
},
case woody_client:call({Service, Fn, Args}, Opts, WoodyCtx) of
{ok, Response} ->
Response;
{exception, Exception} ->
throw(Exception)
end.
get_service_spec(token_authenticator) ->
{tk_token_keeper_thrift, 'TokenAuthenticator'};
get_service_spec({token_authority, _}) ->
{tk_token_keeper_thrift, 'TokenAuthority'};
get_service_spec({token_ephemeral_authority, _}) ->
{tk_token_keeper_thrift, 'EphemeralTokenAuthority'}.
%%
-define(CTX_ENTITY(ID), #bouncer_base_Entity{id = ID}).
encode_context_fragment_content(ContextFragment) ->
Type = {struct, struct, {bouncer_context_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'}},
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),
_ = 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({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).
assert_user({claim_token, _}, undefined) ->
ok;
assert_user({api_key_token, _, _}, undefined) ->
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).
make_auth_expiration(Timestamp) when is_integer(Timestamp) ->
genlib_rfc3339:format(Timestamp, second);
make_auth_expiration(unlimited) ->
undefined.
%%
-include_lib("jose/include/jose_jwk.hrl").
issue_token(Claims, Config) ->
issue_token_with(Claims, get_filename("keys/local/private.pem", Config)).
issue_token_with(Claims, PemFile) ->
JWK = jose_jwk:from_pem_file(PemFile),
JWKPublic = jose_jwk:to_public(JWK),
{_Module, PublicKey} = JWKPublic#jose_jwk.kty,
{_PemEntry, Data, _} = public_key:pem_entry_encode('SubjectPublicKeyInfo', PublicKey),
KID = jose_base64url:encode(crypto:hash(sha256, Data)),
JWT = jose_jwt:sign(JWK, #{<<"alg">> => <<"RS256">>, <<"kid">> => KID}, Claims),
{_Modules, Token} = jose_jws:compact(JWT),
Token.
issue_dummy_token(Config) ->
Claims = #{
<<"jti">> => unique_id(),
<<"sub">> => <<"TEST">>,
<<"exp">> => 0
},
BadPemFile = get_filename("keys/local/dummy.pem", Config),
BadJWK = jose_jwk:from_pem_file(BadPemFile),
GoodPemFile = get_filename("keys/local/private.pem", Config),
GoodJWK = jose_jwk:from_pem_file(GoodPemFile),
JWKPublic = jose_jwk:to_public(GoodJWK),
{_Module, PublicKey} = JWKPublic#jose_jwk.kty,
{_PemEntry, Data, _} = public_key:pem_entry_encode('SubjectPublicKeyInfo', PublicKey),
KID = jose_base64url:encode(crypto:hash(sha256, Data)),
JWT = jose_jwt:sign(BadJWK, #{<<"alg">> => <<"RS256">>, <<"kid">> => KID}, Claims),
{_Modules, Token} = jose_jws:compact(JWT),
Token.
%%
get_filename(Key, Config) ->
filename:join(?config(data_dir, Config), Key).
unique_id() ->
<<ID:64>> = snowflake:new(),
genlib_format:format_int_base(ID, 62).
%%
start_keeper(Authorities) ->
start_keeper(Authorities, undefined).
start_keeper(Env, BlacklistPath) ->
Port = 8022,
Apps = genlib_app:start_application_with(
token_keeper,
[
{port, Port},
{blacklist, #{
path => BlacklistPath
}},
{machinegun, #{
processor => #{
path => <<"/v2/stateproc">>
}
}}
] ++ Env
),
[{keeper_apps, Apps}].
stop_keeper(C) ->
genlib_app:stop_unload_applications(?CONFIG(keeper_apps, C)).
mk_url(Path) ->
mk_url("127.0.0.1", 8022, Path).
mk_url(IP, Port, Path) ->
iolist_to_binary(["http://", IP, ":", genlib:to_binary(Port), Path]).
extract_method_detect_token() ->
{extract_context, #{
methods => [
{detect_token, #{
phony_api_key_opts => #{
metadata_mappings => #{
party_id => ?META_PARTY_ID
}
},
user_session_token_opts => #{
user_realm => <<"external">>,
metadata_mappings => #{
user_id => ?META_USER_ID,
user_email => ?META_USER_EMAIL,
user_realm => ?META_USER_REALM
}
},
user_session_token_origins => [?USER_TOKEN_SOURCE]
}}
]
}}.
extract_method_invoice_tpl_token() ->
{extract_context, #{
methods => [
{invoice_template_access_token, #{
domain => ?TK_RESOURCE_DOMAIN,
metadata_mappings => #{
party_id => ?META_PARTY_ID
}
}}
]
}}.

View File

@ -3,9 +3,9 @@ erlang:
secret_cookie_file: "/opt/machinegun/etc/cookie"
namespaces:
tk_authdata:
apikeymgmt:
processor:
url: http://token-keeper:8022/v1/stateproc/storage
url: http://token-keeper:8022/v2/stateproc
pool_size: 300
storage: