mirror of
https://github.com/valitydev/token-keeper.git
synced 2024-11-06 02:15:21 +00:00
ED-298: Implement new thrift interface (#17)
This commit is contained in:
parent
197b0f786f
commit
30d303f47d
2
Jenkinsfile
vendored
2
Jenkinsfile
vendored
@ -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)
|
||||
|
||||
}
|
||||
|
4
Makefile
4
Makefile
@ -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 \
|
||||
|
@ -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}
|
||||
]}
|
||||
|
||||
].
|
||||
|
@ -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"
|
||||
|
@ -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
63
elvis.config
Normal 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}
|
||||
]
|
||||
}
|
||||
]}
|
||||
]}
|
||||
].
|
151
rebar.config
151
rebar.config
@ -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"]}
|
||||
]}.
|
||||
|
84
rebar.lock
84
rebar.lock
@ -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">>}]}
|
||||
|
@ -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
44
src/tk_authdata.erl
Normal 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}.
|
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -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, #{}}.
|
61
src/tk_authdata_source_legacy_claim.erl
Normal file
61
src/tk_authdata_source_legacy_claim.erl
Normal 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)).
|
@ -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).
|
||||
|
@ -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)}.
|
@ -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.
|
@ -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)).
|
||||
}.
|
47
src/tk_context_extractor.erl
Normal file
47
src/tk_context_extractor.erl
Normal 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
|
46
src/tk_context_extractor_detect_token.erl
Normal file
46
src/tk_context_extractor_detect_token.erl
Normal 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.
|
@ -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}
|
||||
}
|
||||
]
|
45
src/tk_context_extractor_phony_api_key.erl
Normal file
45
src/tk_context_extractor_phony_api_key.erl
Normal 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).
|
@ -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) ->
|
@ -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.
|
@ -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.
|
@ -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
153
src/tk_handler.erl
Normal 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)).
|
144
src/tk_handler_authenticator.erl
Normal file
144
src/tk_handler_authenticator.erl
Normal 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).
|
106
src/tk_handler_authority_ephemeral.erl
Normal file
106
src/tk_handler_authority_ephemeral.erl
Normal 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).
|
168
src/tk_handler_authority_offline.erl
Normal file
168
src/tk_handler_authority_offline.erl
Normal 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).
|
@ -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.
|
@ -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()].
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
143
src/tk_storage_machinegun_schema.erl
Normal file
143
src/tk_storage_machinegun_schema.erl
Normal 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
106
src/tk_token.erl
Normal 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.
|
@ -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.
|
||||
|
@ -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.
|
@ -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
|
||||
).
|
||||
|
@ -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.
|
@ -1 +0,0 @@
|
||||
entries: []
|
@ -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).
|
@ -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-----
|
@ -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-----
|
@ -1,4 +0,0 @@
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAK9fx7qOJT7Aoseu7KKgaLagBh3wvDzg
|
||||
7F/ZMtGbPFikJnnvRWvFB5oEGbMPblvtF0/fjqfu+eqjP3Z1tUSn7TkCAwEAAQ==
|
||||
-----END PUBLIC KEY-----
|
971
test/token_keeper_SUITE.erl
Normal file
971
test/token_keeper_SUITE.erl
Normal 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
|
||||
}
|
||||
}}
|
||||
]
|
||||
}}.
|
@ -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:
|
Loading…
Reference in New Issue
Block a user