From 459278a46aef5dec6aa32acbbf04c2611b263f29 Mon Sep 17 00:00:00 2001 From: Nelson Vides Date: Fri, 10 Sep 2021 15:19:50 +0200 Subject: [PATCH] Make mod_auth_token and mod_keystore multitenancy ready --- big_tests/dynamic_domains.spec | 2 + big_tests/tests/mongoose_helper.erl | 4 +- big_tests/tests/oauth_SUITE.erl | 50 ++++--- src/mod_auth_token.erl | 211 ++++++++++++++-------------- src/mod_auth_token_rdbms.erl | 60 ++++---- src/mod_keystore.erl | 70 ++++----- src/mod_keystore_mnesia.erl | 6 +- src/sasl/cyrsasl_oauth.erl | 3 +- test/auth_tokens_SUITE.erl | 46 +++--- 9 files changed, 223 insertions(+), 229 deletions(-) diff --git a/big_tests/dynamic_domains.spec b/big_tests/dynamic_domains.spec index 39ae112807d..21e314d6e7a 100644 --- a/big_tests/dynamic_domains.spec +++ b/big_tests/dynamic_domains.spec @@ -57,6 +57,8 @@ {suites, "tests", muc_light_http_api_SUITE}. +{suites, "tests", oauth_SUITE}. + {suites, "tests", offline_SUITE}. {suites, "tests", offline_stub_SUITE}. diff --git a/big_tests/tests/mongoose_helper.erl b/big_tests/tests/mongoose_helper.erl index 62afe9214d0..c9915390ff0 100644 --- a/big_tests/tests/mongoose_helper.erl +++ b/big_tests/tests/mongoose_helper.erl @@ -451,8 +451,8 @@ supports_sasl_module(Module) -> rpc(mim(), ejabberd_auth, supports_sasl_module, [Host, Module]). backup_auth_config(Config) -> - XMPPDomain = escalus_ejabberd:unify_str_arg(ct:get_config({hosts, mim, domain})), - AuthOpts = rpc(mim(), ejabberd_config, get_local_option, [{auth_opts, XMPPDomain}]), + HostType = domain_helper:host_type(), + AuthOpts = rpc(mim(), ejabberd_config, get_local_option, [{auth_opts, HostType}]), [{auth_opts, AuthOpts} | Config]. backup_sasl_mechanisms_config(Config) -> diff --git a/big_tests/tests/oauth_SUITE.erl b/big_tests/tests/oauth_SUITE.erl index 10ac102caf9..6ecfba8fcd6 100644 --- a/big_tests/tests/oauth_SUITE.erl +++ b/big_tests/tests/oauth_SUITE.erl @@ -77,10 +77,10 @@ suite() -> %%-------------------------------------------------------------------- init_per_suite(Config0) -> - case mongoose_helper:is_rdbms_enabled(domain()) of + case mongoose_helper:is_rdbms_enabled(domain_helper:host_type()) of true -> Config = dynamic_modules:stop_running(mod_last, Config0), - Host = ct:get_config({hosts, mim, domain}), + HostType = domain_helper:host_type(), KeyStoreOpts = [{keys, [ {token_secret, ram}, %% This is a hack for tests! As the name implies, @@ -92,18 +92,18 @@ init_per_suite(Config0) -> ]}], AuthOpts = [{ {validity_period, access}, {60, minutes} }, { {validity_period, refresh}, {1, days} }], - dynamic_modules:start(Host, mod_keystore, KeyStoreOpts), - dynamic_modules:start(Host, mod_auth_token, AuthOpts), + dynamic_modules:start(HostType, mod_keystore, KeyStoreOpts), + dynamic_modules:start(HostType, mod_auth_token, AuthOpts), escalus:init_per_suite([{auth_opts, AuthOpts} | Config]); false -> {skip, "RDBMS not available"} end. end_per_suite(Config) -> - Host = ct:get_config({hosts, mim, domain}), + HostType = domain_helper:host_type(), dynamic_modules:start_running(Config), - dynamic_modules:stop(Host, mod_auth_token), - dynamic_modules:stop(Host, mod_keystore), + dynamic_modules:stop(HostType, mod_auth_token), + dynamic_modules:stop(HostType, mod_keystore), escalus:end_per_suite(Config). init_per_group(GroupName, Config0) -> @@ -123,19 +123,19 @@ end_per_group(_GroupName, Config) -> mongoose_helper:restore_auth_config(Config), escalus:delete_users(Config, escalus:get_users([bob, alice])). -init_per_testcase(check_for_oauth_with_mod_auth_token_not_loaded = CaseName, Config) -> - Host = ct:get_config({hosts, mim, domain}), - dynamic_modules:stop(Host, mod_auth_token), +init_per_testcase(check_for_oauth_with_mod_auth_token_not_loaded, Config) -> + HostType = domain_helper:host_type(), + dynamic_modules:stop(HostType, mod_auth_token), init_per_testcase(generic, Config); init_per_testcase(CaseName, Config) -> clean_token_db(), escalus:init_per_testcase(CaseName, Config). -end_per_testcase(check_for_oauth_with_mod_auth_token_not_loaded = CaseName, Config) -> - Host = ct:get_config({hosts, mim, domain}), - AuthOpts = proplists:get_value(auth_opts, Config), - dynamic_modules:start(Host, mod_auth_token, AuthOpts), +end_per_testcase(check_for_oauth_with_mod_auth_token_not_loaded, Config) -> + HostType = domain_helper:host_type(), + AuthOpts = ?config(auth_opts, Config), + dynamic_modules:start(HostType, mod_auth_token, AuthOpts), end_per_testcase(generic, Config); end_per_testcase(CaseName, Config) -> clean_token_db(), @@ -172,12 +172,13 @@ token_login_failure(Config, User, Token) -> get_revoked_token(Config, UserName) -> BJID = escalus_users:get_jid(Config, UserName), JID = rpc(mim(), jid, from_binary, [BJID]), - Token = rpc(mim(), mod_auth_token, token, [refresh, JID]), - ValidSeqNo = rpc(mim(), mod_auth_token_rdbms, get_valid_sequence_number, [JID]), + HostType = domain_helper:host_type(), + Token = rpc(mim(), mod_auth_token, token, [HostType, JID, refresh]), + ValidSeqNo = rpc(mim(), mod_auth_token_rdbms, get_valid_sequence_number, [HostType, JID]), RevokedToken0 = record_set(Token, [{5, invalid_sequence_no(ValidSeqNo)}, {7, undefined}, {8, undefined}]), - RevokedToken = rpc(mim(), mod_auth_token, token_with_mac, [RevokedToken0]), + RevokedToken = rpc(mim(), mod_auth_token, token_with_mac, [HostType, RevokedToken0]), rpc(mim(), mod_auth_token, serialize, [RevokedToken]). invalid_sequence_no(SeqNo) -> @@ -293,7 +294,7 @@ get_owner_seqno_to_revoke(Config, User) -> {Owner, binary_to_integer(SeqNo), RefreshToken}. revoke_token(Owner) -> - rpc(mim(), mod_auth_token, revoke, [Owner]). + rpc(mim(), mod_auth_token, revoke, [domain_helper:host_type(), Owner]). revoke_token_cmd_when_no_token(Config) -> %% given existing user with no token @@ -313,7 +314,7 @@ revoke_token_cmd(Config) -> token_removed_on_user_removal(Config) -> %% given existing user with token and XMPP (de)registration available _Tokens = request_tokens_once_logged_in_impl(Config, bob), - true = is_xmpp_registration_available(escalus_users:get_server(Config, bob)), + true = is_xmpp_registration_available(domain_helper:host_type()), %% when user account is deleted S = fun (Bob) -> IQ = escalus_stanza:remove_account(), @@ -382,7 +383,8 @@ verify_format(GroupName, {_User, Props}) -> Server = proplists:get_value(server, Props), Password = proplists:get_value(password, Props), JID = mongoose_helper:make_jid(Username, Server), - {SPassword, _} = rpc(mim(), ejabberd_auth, get_passterm_with_authmodule, [host_type(), JID]), + {SPassword, _} = rpc(mim(), ejabberd_auth, get_passterm_with_authmodule, + [domain_helper:host_type(), JID]), do_verify_format(GroupName, Password, SPassword). do_verify_format(login_scram, _Password, SPassword) -> @@ -411,8 +413,7 @@ convert_arg(S) when is_list(S) -> S. clean_token_db() -> Q = [<<"DELETE FROM auth_token">>], - RDBMSHost = domain(), %% mam is also tested against local rdbms - {updated, _} = rpc(mim(), mongoose_rdbms, sql_query, [RDBMSHost, Q]). + {updated, _} = rpc(mim(), mongoose_rdbms, sql_query, [domain_helper:host_type(), Q]). get_users_token(C, User) -> Q = ["SELECT * FROM auth_token at " @@ -459,7 +460,7 @@ make_provision_token(Config, User, VCard) -> undefined, %% body undefined}, - T = rpc(mim(), mod_auth_token, token_with_mac, [T0]), + T = rpc(mim(), mod_auth_token, token_with_mac, [domain_helper:host_type(), T0]), %% assert no RPC error occured {token, provision} = {element(1, T), element(2, T)}, serialize(T). @@ -474,8 +475,5 @@ serialize(ServerSideToken) -> to_lower(B) when is_binary(B) -> list_to_binary(string:to_lower(binary_to_list(B))). -host_type() -> - domain(). - domain() -> ct:get_config({hosts, mim, domain}). diff --git a/src/mod_auth_token.erl b/src/mod_auth_token.erl index 640dfecdead..92b9669503f 100644 --- a/src/mod_auth_token.erl +++ b/src/mod_auth_token.erl @@ -10,9 +10,10 @@ -include("mongoose_config_spec.hrl"). %% gen_mod callbacks --export([start/2, - stop/1, - config_spec/0]). +-export([start/2]). +-export([stop/1]). +-export([supported_features/0]). +-export([config_spec/0]). %% Config spec callbacks -export([process_validity_period/1]). @@ -22,12 +23,12 @@ disco_local_features/1]). %% gen_iq_handler handlers --export([process_iq/4]). +-export([process_iq/5]). %% Public API --export([authenticate/1, - revoke/1, - token/2]). +-export([authenticate/2, + revoke/2, + token/3]). %% Token serialization -export([deserialize/1, @@ -40,8 +41,8 @@ -export([datetime_to_seconds/1, seconds_to_datetime/1]). -export([expiry_datetime/3, - get_key_for_user/2, - token_with_mac/1]). + get_key_for_host_type/2, + token_with_mac/2]). -export([config_metrics/1]). @@ -52,15 +53,14 @@ -define(MOD_AUTH_TOKEN_BACKEND, mod_auth_token_backend). -ignore_xref([ - {?MOD_AUTH_TOKEN_BACKEND, clean_tokens, 1}, - {?MOD_AUTH_TOKEN_BACKEND, get_valid_sequence_number, 1}, - {?MOD_AUTH_TOKEN_BACKEND, revoke, 1}, {?MOD_AUTH_TOKEN_BACKEND, start, 1}, - {?MOD_AUTH_TOKEN_BACKEND, get_valid_sequence_number, 1}, + {?MOD_AUTH_TOKEN_BACKEND, revoke, 2}, + {?MOD_AUTH_TOKEN_BACKEND, get_valid_sequence_number, 2}, + {?MOD_AUTH_TOKEN_BACKEND, clean_tokens, 2}, behaviour_info/1, clean_tokens/3, datetime_to_seconds/1, deserialize/1, - disco_local_features/1, expiry_datetime/3, get_key_for_user/2, process_iq/4, - revoke/1, revoke_token_command/1, seconds_to_datetime/1, serialize/1, token/2, - token_with_mac/1 + disco_local_features/1, expiry_datetime/3, get_key_for_host_type/2, process_iq/5, + revoke/2, revoke_token_command/1, seconds_to_datetime/1, serialize/1, token/3, + token_with_mac/2 ]). -type error() :: error | {error, any()}. @@ -74,17 +74,13 @@ | {ok, module(), jid:user(), binary()} | error(). --callback start(LServer) -> ok when - LServer :: jid:lserver(). +-callback start(mongooseim:host_type()) -> ok. --callback revoke(Owner) -> ok | not_found when - Owner :: jid:jid(). +-callback revoke(mongooseim:host_type(), jid:jid()) -> ok | not_found. --callback get_valid_sequence_number(Owner) -> integer() when - Owner :: jid:jid(). +-callback get_valid_sequence_number(mongooseim:host_type(), jid:jid()) -> integer(). --callback clean_tokens(Owner) -> ok when - Owner :: jid:jid(). +-callback clean_tokens(mongooseim:host_type(), jid:jid()) -> ok. -define(A2B(A), atom_to_binary(A, utf8)). @@ -95,25 +91,32 @@ %% gen_mod callbacks %% --spec start(jid:server(), list()) -> ok. -start(Domain, Opts) -> - gen_mod:start_backend_module(?MODULE, default_opts(Opts)), - mod_auth_token_backend:start(Domain), +-spec start(mongooseim:host_type(), gen_mod:module_opts()) -> ok. +start(HostType, Opts) -> IQDisc = gen_mod:get_opt(iqdisc, Opts, no_queue), - [ ejabberd_hooks:add(Hook, Domain, ?MODULE, Handler, Priority) - || {Hook, Handler, Priority} <- hook_handlers() ], - gen_iq_handler:add_iq_handler(ejabberd_sm, Domain, ?NS_ESL_TOKEN_AUTH, - ?MODULE, process_iq, IQDisc), + gen_mod:start_backend_module(?MODULE, default_opts(Opts)), + mod_auth_token_backend:start(HostType), + ejabberd_hooks:add(hooks(HostType)), + gen_iq_handler:add_iq_handler_for_domain( + HostType, ?NS_ESL_TOKEN_AUTH, ejabberd_sm, + fun ?MODULE:process_iq/5, #{}, IQDisc), ejabberd_commands:register_commands(commands()), ok. --spec stop(jid:server()) -> ok. -stop(Domain) -> - gen_iq_handler:remove_iq_handler(ejabberd_sm, Domain, ?NS_ESL_TOKEN_AUTH), - [ ejabberd_hooks:delete(Hook, Domain, ?MODULE, Handler, Priority) - || {Hook, Handler, Priority} <- hook_handlers() ], +-spec stop(mongooseim:host_type()) -> ok. +stop(HostType) -> + gen_iq_handler:remove_iq_handler_for_domain(HostType, ?NS_ESL_TOKEN_AUTH, ejabberd_sm), + ejabberd_hooks:delete(hooks(HostType)), ok. +hooks(HostType) -> + [{remove_user, HostType, ?MODULE, clean_tokens, 50}, + {disco_local_features, HostType, ?MODULE, disco_local_features, 90}]. + +-spec supported_features() -> [atom()]. +supported_features() -> + [dynamic_domains]. + -spec config_spec() -> mongoose_config_spec:config_section(). config_spec() -> #section{ @@ -144,10 +147,6 @@ process_validity_period(KVs) -> default_opts(Opts) -> [{backend, rdbms} || not proplists:is_defined(backend, Opts)] ++ Opts. -hook_handlers() -> - [{remove_user, clean_tokens, 50}, - {disco_local_features, disco_local_features, 90}]. - -spec commands() -> [ejabberd_commands:cmd()]. commands() -> [#ejabberd_commands{ name = revoke_token, tags = [tokens], @@ -168,18 +167,18 @@ serialize(#token{token_body = Body, mac_signature = MAC}) -> %% #token{} contains fields which are: %% - primary - these have to be supplied on token creation, %% - dependent - these are computed based on the primary fields. -%% `token_with_mac/1` computes dependent fields and stores them in the record +%% `token_with_mac/2` computes dependent fields and stores them in the record %% based on a record with just the primary fields. --spec token_with_mac(token()) -> token(). -token_with_mac(#token{mac_signature = undefined, token_body = undefined} = T) -> +-spec token_with_mac(mongooseim:host_type(), token()) -> token(). +token_with_mac(HostType, #token{mac_signature = undefined, token_body = undefined} = T) -> Body = join_fields(T), - MAC = keyed_hash(Body, user_hmac_opts(T#token.type, T#token.user_jid)), + MAC = keyed_hash(Body, user_hmac_opts(HostType, T#token.type)), T#token{token_body = Body, mac_signature = MAC}. --spec user_hmac_opts(token_type(), jid:jid()) -> [{any(), any()}]. -user_hmac_opts(TokenType, User) -> +-spec user_hmac_opts(mongooseim:host_type(), token_type()) -> [{any(), any()}]. +user_hmac_opts(HostType, TokenType) -> lists:keystore(key, 1, hmac_opts(), - {key, get_key_for_user(TokenType, User)}). + {key, get_key_for_host_type(HostType, TokenType)}). field_separator() -> 0. @@ -216,11 +215,10 @@ hmac_opts() -> deserialize(Serialized) when is_binary(Serialized) -> get_token_as_record(Serialized). --spec revoke(Owner) -> ok | not_found | error when - Owner :: jid:jid(). -revoke(Owner) -> +-spec revoke(mongooseim:host_type(), jid:jid()) -> ok | not_found | error. +revoke(HostType, Owner) -> try - mod_auth_token_backend:revoke(Owner) + mod_auth_token_backend:revoke(HostType, Owner) catch Class:Reason:Stacktrace -> ?LOG_ERROR(#{what => auth_token_revoke_failed, @@ -229,17 +227,17 @@ revoke(Owner) -> error end. --spec authenticate(serialized()) -> validation_result(). -authenticate(SerializedToken) -> +-spec authenticate(mongooseim:host_type(), serialized()) -> validation_result(). +authenticate(HostType, SerializedToken) -> try - do_authenticate(SerializedToken) + do_authenticate(HostType, SerializedToken) catch _:_ -> {error, internal_server_error} end. -do_authenticate(SerializedToken) -> +do_authenticate(HostType, SerializedToken) -> #token{user_jid = Owner} = Token = deserialize(SerializedToken), - {Criteria, Result} = validate_token(Token), + {Criteria, Result} = validate_token(HostType, Token), ?LOG_INFO(#{what => auth_token_validate, user => Owner#jid.luser, server => Owner#jid.lserver, criteria => Criteria, result => Result}), @@ -247,14 +245,14 @@ do_authenticate(SerializedToken) -> {ok, access} -> {ok, mod_auth_token, Owner#jid.luser}; {ok, refresh} -> - case token(access, Owner) of + case token(HostType, Owner, access) of #token{} = T -> {ok, mod_auth_token, Owner#jid.luser, serialize(T)}; {error, R} -> {error, R} end; {ok, provision} -> - case set_vcard(Owner#jid.lserver, Owner, Token#token.vcard) of + case set_vcard(HostType, Owner, Token#token.vcard) of {error, Reason} -> ?LOG_WARNING(#{what => auth_token_set_vcard_failed, reason => Reason, token_vcard => Token#token.vcard, @@ -269,33 +267,33 @@ do_authenticate(SerializedToken) -> || {_, false} = Criterion <- Criteria ]}} end. -set_vcard(Domain, #jid{} = User, #xmlel{} = VCard) -> - mongoose_hooks:set_vcard(Domain, User, VCard). +set_vcard(HostType, #jid{} = User, #xmlel{} = VCard) -> + mongoose_hooks:set_vcard(HostType, User, VCard). -validate_token(Token) -> - Criteria = [{mac_valid, is_mac_valid(Token)}, +validate_token(HostType, Token) -> + Criteria = [{mac_valid, is_mac_valid(HostType, Token)}, {not_expired, is_not_expired(Token)}, - {not_revoked, not is_revoked(Token)}], + {not_revoked, not is_revoked(Token, HostType)}], Result = case Criteria of [{_, true}, {_, true}, {_, true}] -> ok; _ -> error end, {Criteria, Result}. -is_mac_valid(#token{type = Type, user_jid = Owner, +is_mac_valid(HostType, #token{type = Type, user_jid = Owner, token_body = Body, mac_signature = ReceivedMAC}) -> - ComputedMAC = keyed_hash(Body, user_hmac_opts(Type, Owner)), + ComputedMAC = keyed_hash(Body, user_hmac_opts(HostType, Type)), ReceivedMAC =:= ComputedMAC. is_not_expired(#token{expiry_datetime = Expiry}) -> utc_now_as_seconds() < datetime_to_seconds(Expiry). -is_revoked(#token{type = T}) when T =:= access; +is_revoked(#token{type = T}, _) when T =:= access; T =:= provision -> false; -is_revoked(#token{type = refresh, sequence_no = TokenSeqNo} = T) -> +is_revoked(#token{type = refresh, sequence_no = TokenSeqNo} = T, HostType) -> try - ValidSeqNo = mod_auth_token_backend:get_valid_sequence_number(T#token.user_jid), + ValidSeqNo = mod_auth_token_backend:get_valid_sequence_number(HostType, T#token.user_jid), TokenSeqNo < ValidSeqNo catch Class:Reason:Stacktrace -> @@ -306,18 +304,16 @@ is_revoked(#token{type = refresh, sequence_no = TokenSeqNo} = T) -> true end. --spec process_iq(jid:jid(), mongoose_acc:t(), jid:jid(), jlib:iq()) -> {mongoose_acc:t(), jlib:iq()} | error(). -process_iq(From, To, Acc, #iq{xmlns = ?NS_ESL_TOKEN_AUTH} = IQ) -> - IQResp = case lists:member(From#jid.lserver, ?MYHOSTS) of - true -> process_local_iq(From, To, IQ); - false -> iq_error(IQ, [mongoose_xmpp_errors:item_not_found()]) - end, +-spec process_iq(mongoose_acc:t(), jid:jid(), jid:jid(), jlib:iq(), any()) -> + {mongoose_acc:t(), jlib:iq()} | error(). +process_iq(Acc, From, To, #iq{xmlns = ?NS_ESL_TOKEN_AUTH} = IQ, _Extra) -> + IQResp = process_local_iq(Acc, From, To, IQ), {Acc, IQResp}; -process_iq(_From, _To, Acc, #iq{} = IQ) -> +process_iq(Acc, _From, _To, #iq{} = IQ, _Extra) -> {Acc, iq_error(IQ, [mongoose_xmpp_errors:bad_request()])}. -process_local_iq(From, _To, IQ) -> - try create_token_response(From, IQ) of +process_local_iq(Acc, From, _To, IQ) -> + try create_token_response(Acc, From, IQ) of #iq{} = Response -> Response; {error, Reason} -> iq_error(IQ, [Reason]) catch @@ -327,8 +323,9 @@ process_local_iq(From, _To, IQ) -> iq_error(IQ, SubElements) when is_list(SubElements) -> IQ#iq{type = error, sub_el = SubElements}. -create_token_response(From, IQ) -> - case {token(access, From), token(refresh, From)} of +create_token_response(Acc, From, IQ) -> + HostType = mongoose_acc:host_type(Acc), + case {token(HostType, From, access), token(HostType, From, refresh)} of {#token{} = AccessToken, #token{} = RefreshToken} -> IQ#iq{type = result, sub_el = [#xmlel{name = <<"items">>, @@ -349,17 +346,18 @@ seconds_to_datetime(Seconds) -> utc_now_as_seconds() -> datetime_to_seconds(calendar:universal_time()). --spec token(token_type(), jid:jid()) -> token() | error(). -token(Type, User) -> - ExpiryTime = expiry_datetime(User#jid.lserver, Type, utc_now_as_seconds()), +-spec token(mongooseim:host_type(), jid:jid(), token_type()) -> token() | error(). +token(HostType, User, Type) -> + ExpiryTime = expiry_datetime(HostType, Type, utc_now_as_seconds()), T = #token{type = Type, expiry_datetime = ExpiryTime, user_jid = User}, try - token_with_mac(case Type of - access -> T; - refresh -> - ValidSeqNo = mod_auth_token_backend:get_valid_sequence_number(User), - T#token{sequence_no = ValidSeqNo} - end) + T2 = case Type of + access -> T; + refresh -> + ValidSeqNo = mod_auth_token_backend:get_valid_sequence_number(HostType, User), + T#token{sequence_no = ValidSeqNo} + end, + token_with_mac(HostType, T2) catch Class:Reason:Stacktrace -> ?LOG_ERROR(#{what => auth_token_revocation_check_failed, @@ -367,25 +365,22 @@ token(Type, User) -> token_type => Type, expiry_datetime => ExpiryTime, user => User#jid.luser, server => User#jid.lserver, class => Class, reason => Reason, stacktrace => Stacktrace}), - {error, {Class, Reason}} + {error, {Class, Reason}} end. %% {modules, [ %% {mod_auth_token, [{{validity_period, access}, {13, minutes}}, %% {{validity_period, refresh}, {13, days}}]} %% ]}. --spec expiry_datetime(Domain, Type, UTCSeconds) -> ExpiryDatetime when - Domain :: jid:server(), - Type :: token_type(), - UTCSeconds :: non_neg_integer(), - ExpiryDatetime :: calendar:datetime(). -expiry_datetime(Domain, Type, UTCSeconds) -> - Period = get_validity_period(Domain, Type), +-spec expiry_datetime(mongooseim:host_type(), token_type(), non_neg_integer()) -> + calendar:datetime(). +expiry_datetime(HostType, Type, UTCSeconds) -> + Period = get_validity_period(HostType, Type), seconds_to_datetime(UTCSeconds + period_to_seconds(Period)). --spec get_validity_period(jid:server(), token_type()) -> period(). -get_validity_period(Domain, Type) -> - gen_mod:get_module_opt(Domain, ?MODULE, {validity_period, Type}, +-spec get_validity_period(mongooseim:host_type(), token_type()) -> period(). +get_validity_period(HostType, Type) -> + gen_mod:get_module_opt(HostType, ?MODULE, {validity_period, Type}, default_validity_period(Type)). period_to_seconds({Days, days}) -> milliseconds_to_seconds(timer:hours(24 * Days)); @@ -438,11 +433,10 @@ decode_token_type(<<"refresh">>) -> decode_token_type(<<"provision">>) -> provision. --spec get_key_for_user(token_type(), jid:jid()) -> binary(). -get_key_for_user(TokenType, User) -> - UsersHost = User#jid.lserver, +-spec get_key_for_host_type(mongooseim:host_type(), token_type()) -> binary(). +get_key_for_host_type(HostType, TokenType) -> KeyName = key_name(TokenType), - [{{KeyName, UsersHost}, RawKey}] = mongoose_hooks:get_key(UsersHost, KeyName), + [{{KeyName, UsersHost}, RawKey}] = mongoose_hooks:get_key(HostType, KeyName), RawKey. -spec key_name(token_type()) -> token_secret | provision_pre_shared. @@ -455,7 +449,9 @@ key_name(provision) -> provision_pre_shared. ResCode :: ok | not_found | error, ResTuple :: {ResCode, string()}. revoke_token_command(Owner) -> - try revoke(jid:from_binary(Owner)) of + #jid{lserver = LServer} = Jid = jid:from_binary(Owner), + {ok, HostType} = mongoose_domain_api:get_domain_host_type(LServer), + try revoke(HostType, Jid) of not_found -> {not_found, "User or token not found."}; ok -> @@ -469,9 +465,10 @@ revoke_token_command(Owner) -> -spec clean_tokens(mongoose_acc:t(), User :: jid:user(), Server :: jid:server()) -> mongoose_acc:t(). clean_tokens(Acc, User, Server) -> + HostType = mongoose_acc:host_type(Acc), + Owner = jid:make(User, Server, <<>>), try - Owner = jid:make(User, Server, <<>>), - mod_auth_token_backend:clean_tokens(Owner) + mod_auth_token_backend:clean_tokens(HostType, Owner) catch Class:Reason:Stacktrace -> ?LOG_ERROR(#{what => auth_token_clean_tokens_failed, diff --git a/src/mod_auth_token_rdbms.erl b/src/mod_auth_token_rdbms.erl index 670ca3e3ce8..afe3d2a7ef6 100644 --- a/src/mod_auth_token_rdbms.erl +++ b/src/mod_auth_token_rdbms.erl @@ -2,68 +2,64 @@ -behaviour(mod_auth_token). -export([start/1, - get_valid_sequence_number/1, - revoke/1, - clean_tokens/1]). + get_valid_sequence_number/2, + revoke/2, + clean_tokens/2]). --include("jlib.hrl"). -include("mongoose.hrl"). --spec start(jid:lserver()) -> ok. -start(LServer) -> - prepare_queries(LServer). +-spec start(mongooseim:host_type()) -> ok. +start(HostType) -> + prepare_queries(HostType). %% Assumption: all sequence numbers less than the current valid one %% are not valid. --spec get_valid_sequence_number(JID) -> integer() when - JID :: jid:jid(). -get_valid_sequence_number(#jid{lserver = LServer} = JID) -> +-spec get_valid_sequence_number(mongooseim:host_type(), jid:jid()) -> integer(). +get_valid_sequence_number(HostType, JID) -> BBareJID = jid:to_binary(jid:to_bare(JID)), {atomic, Selected} = mongoose_rdbms:sql_transaction( - LServer, fun() -> get_sequence_number_t(LServer, BBareJID) end), + HostType, fun() -> get_sequence_number_t(HostType, BBareJID) end), mongoose_rdbms:selected_to_integer(Selected). --spec revoke(JID) -> ok | not_found when - JID :: jid:jid(). -revoke(#jid{lserver = LServer} = JID) -> +-spec revoke(mongooseim:host_type(), jid:jid()) -> ok | not_found. +revoke(HostType, JID) -> BBareJID = jid:to_binary(jid:to_bare(JID)), - QueryResult = execute_revoke_token(LServer, BBareJID), + QueryResult = execute_revoke_token(HostType, BBareJID), ?LOG_DEBUG(#{what => auth_token_revoke, owner => BBareJID, sql_result => QueryResult}), case QueryResult of {updated, 1} -> ok; {updated, 0} -> not_found end. --spec clean_tokens(Owner) -> ok when - Owner :: jid:jid(). -clean_tokens(#jid{lserver = LServer} = Owner) -> +-spec clean_tokens(mongooseim:host_type(), jid:jid()) -> ok. +clean_tokens(HostType, Owner) -> BBareJID = jid:to_binary(jid:to_bare(Owner)), - execute_delete_token(LServer, BBareJID), + execute_delete_token(HostType, BBareJID), ok. --spec prepare_queries(jid:lserver()) -> ok. -prepare_queries(LServer) -> +-spec prepare_queries(mongooseim:host_type()) -> ok. +prepare_queries(HostType) -> mongoose_rdbms:prepare(auth_token_select, auth_token, [owner], <<"SELECT seq_no FROM auth_token WHERE owner = ?">>), mongoose_rdbms:prepare(auth_token_revoke, auth_token, [owner], <<"UPDATE auth_token SET seq_no=seq_no+1 WHERE owner = ?">>), mongoose_rdbms:prepare(auth_token_delete, auth_token, [owner], <<"DELETE from auth_token WHERE owner = ?">>), - rdbms_queries:prepare_upsert(LServer, auth_token_upsert, auth_token, + rdbms_queries:prepare_upsert(HostType, auth_token_upsert, auth_token, [<<"owner">>, <<"seq_no">>], [], [<<"owner">>]), ok. --spec execute_revoke_token(jid:lserver(), jid:literal_jid()) -> mongoose_rdbms:query_result(). -execute_revoke_token(LServer, Owner) -> - mongoose_rdbms:execute_successfully(LServer, auth_token_revoke, [Owner]). +-spec execute_revoke_token(mongooseim:host_type(), jid:literal_jid()) -> mongoose_rdbms:query_result(). +execute_revoke_token(HostType, Owner) -> + mongoose_rdbms:execute_successfully(HostType, auth_token_revoke, [Owner]). --spec execute_delete_token(jid:lserver(), jid:literal_jid()) -> mongoose_rdbms:query_result(). -execute_delete_token(LServer, Owner) -> - mongoose_rdbms:execute_successfully(LServer, auth_token_delete, [Owner]). +-spec execute_delete_token(mongooseim:host_type(), jid:literal_jid()) -> mongoose_rdbms:query_result(). +execute_delete_token(HostType, Owner) -> + mongoose_rdbms:execute_successfully(HostType, auth_token_delete, [Owner]). --spec get_sequence_number_t(jid:lserver(), jid:literal_jid()) -> mongoose_rdbms:query_result(). -get_sequence_number_t(LServer, Owner) -> +-spec get_sequence_number_t(mongooseim:host_type(), jid:literal_jid()) -> mongoose_rdbms:query_result(). +get_sequence_number_t(HostType, Owner) -> {updated, _} = - rdbms_queries:execute_upsert(LServer, auth_token_upsert, [Owner, 1], [], [Owner]), - mongoose_rdbms:execute_successfully(LServer, auth_token_select, [Owner]). + rdbms_queries:execute_upsert(HostType, auth_token_upsert, [Owner, 1], [], [Owner]), + mongoose_rdbms:execute_successfully(HostType, auth_token_select, [Owner]). diff --git a/src/mod_keystore.erl b/src/mod_keystore.erl index a3034cc6d86..16cc573198a 100644 --- a/src/mod_keystore.erl +++ b/src/mod_keystore.erl @@ -4,9 +4,10 @@ -behaviour(mongoose_module_metrics). %% gen_mod callbacks --export([start/2, - stop/1, - config_spec/0]). +-export([start/2]). +-export([stop/1]). +-export([supported_features/0]). +-export([config_spec/0]). %% Hook handlers -export([get_key/2]). @@ -54,17 +55,15 @@ -type key() :: #key{id :: key_id(), key :: raw_key()}. --callback init(Domain, Opts) -> ok when - Domain :: jid:server(), - Opts :: [any()]. +-callback init(mongooseim:host_type(), gen_mod:module_opts()) -> ok. %% Cluster members race to decide whose key gets stored in the distributed database. %% That's why ProposedKey (the key this cluster member tries to propagate to other nodes) %% might not be the same as ActualKey (key of the member who will have won the race). -callback init_ram_key(ProposedKey) -> Result when - ProposedKey :: mod_keystore:key(), + ProposedKey :: key(), Result :: {ok, ActualKey} | {error, any()}, - ActualKey :: mod_keystore:key(). + ActualKey :: key(). -callback get_key(ID :: key_id()) -> key_list(). @@ -72,24 +71,31 @@ %% gen_mod callbacks %% --spec start(jid:server(), list()) -> ok. -start(Domain, Opts) -> +-spec start(mongooseim:host_type(), list()) -> ok. +start(HostType, Opts) -> validate_opts(Opts), create_keystore_ets(), gen_mod:start_backend_module(?MODULE, Opts), - mod_keystore_backend:init(Domain, Opts), - init_keys(Domain, Opts), - [ ejabberd_hooks:add(Hook, Domain, ?MODULE, Handler, Priority) - || {Hook, Handler, Priority} <- hook_handlers() ], + mod_keystore_backend:init(HostType, Opts), + init_keys(HostType, Opts), + ejabberd_hooks:add(hooks(HostType)), ok. --spec stop(jid:server()) -> ok. -stop(Domain) -> - [ ejabberd_hooks:delete(Hook, Domain, ?MODULE, Handler, Priority) - || {Hook, Handler, Priority} <- hook_handlers() ], - clear_keystore_ets(Domain), +-spec stop(mongooseim:host_type()) -> ok. +stop(HostType) -> + ejabberd_hooks:delete(hooks(HostType)), + clear_keystore_ets(HostType), ok. +hooks(HostType) -> + [ + {get_key, HostType, ?MODULE, get_key, 50} + ]. + +-spec supported_features() -> [atom()]. +supported_features() -> + [dynamic_domains]. + -spec config_spec() -> mongoose_config_spec:config_section(). config_spec() -> #section{ @@ -148,11 +154,6 @@ get_key(HandlerAcc, KeyID) -> %% Internal functions %% -hook_handlers() -> - [ - {get_key, get_key, 50} - ]. - create_keystore_ets() -> case does_table_exist(keystore) of true -> ok; @@ -160,7 +161,7 @@ create_keystore_ets() -> BaseOpts = [named_table, public, {read_concurrency, true}], Opts = maybe_add_heir(whereis(ejabberd_sup), self(), BaseOpts), - keystore = ets:new(keystore, Opts), + ets:new(keystore, Opts), ok end. @@ -175,23 +176,24 @@ maybe_add_heir(EjdSupPid, _Self, BaseOpts) when is_pid(EjdSupPid) -> maybe_add_heir(_, _, BaseOpts) -> BaseOpts. -clear_keystore_ets(Domain) -> - Pattern = {{'_', Domain}, '$1'}, +clear_keystore_ets(HostType) -> + Pattern = {{'_', HostType}, '$1'}, ets:match_delete(keystore, Pattern). + does_table_exist(NameOrTID) -> ets:info(NameOrTID, name) /= undefined. -init_keys(Domain, Opts) -> - [ init_key(K, Domain, Opts) || K <- proplists:get_value(keys, Opts, []) ]. +init_keys(HostType, Opts) -> + [ init_key(K, HostType, Opts) || K <- proplists:get_value(keys, Opts, []) ]. --spec init_key({key_name(), key_type()}, jid:server(), list()) -> ok. -init_key({KeyName, {file, Path}}, Domain, _Opts) -> +-spec init_key({key_name(), key_type()}, mongooseim:host_type(), list()) -> ok. +init_key({KeyName, {file, Path}}, HostType, _Opts) -> {ok, Data} = file:read_file(Path), - true = ets_store_key({KeyName, Domain}, Data), + true = ets_store_key({KeyName, HostType}, Data), ok; -init_key({KeyName, ram}, Domain, Opts) -> +init_key({KeyName, ram}, HostType, Opts) -> ProposedKey = crypto:strong_rand_bytes(get_key_size(Opts)), - KeyRecord = #key{id = {KeyName, Domain}, + KeyRecord = #key{id = {KeyName, HostType}, key = ProposedKey}, {ok, _ActualKey} = mod_keystore_backend:init_ram_key(KeyRecord), ok. diff --git a/src/mod_keystore_mnesia.erl b/src/mod_keystore_mnesia.erl index e9b3e8c43c4..9643faa0475 100644 --- a/src/mod_keystore_mnesia.erl +++ b/src/mod_keystore_mnesia.erl @@ -8,10 +8,8 @@ -include("mod_keystore.hrl"). --spec init(Domain, Opts) -> ok when - Domain :: jid:server(), - Opts :: [any()]. -init(_Domain, _Opts) -> +-spec init(mongooseim:host_type(), gen_mod:module_opts()) -> ok. +init(_HostType, _Opts) -> mnesia:create_table(key, [{ram_copies, [node()]}, {type, set}, diff --git a/src/sasl/cyrsasl_oauth.erl b/src/sasl/cyrsasl_oauth.erl index 5a4c19f3e1a..3f39cf16bc3 100644 --- a/src/sasl/cyrsasl_oauth.erl +++ b/src/sasl/cyrsasl_oauth.erl @@ -24,7 +24,8 @@ mech_new(_Host, Creds, _Socket) -> | {error, binary()}. mech_step(#state{creds = Creds}, SerializedToken) -> %% SerializedToken is a token decoded from CDATA body sent by client - case mod_auth_token:authenticate(SerializedToken) of + HostType = mongoose_credentials:host_type(Creds), + case mod_auth_token:authenticate(HostType, SerializedToken) of % Validating access token {ok, AuthModule, User} -> {ok, mongoose_credentials:extend(Creds, diff --git a/test/auth_tokens_SUITE.erl b/test/auth_tokens_SUITE.erl index 60c0ae6d6df..52d8040cd1d 100644 --- a/test/auth_tokens_SUITE.erl +++ b/test/auth_tokens_SUITE.erl @@ -129,7 +129,7 @@ validation_test(_, ExampleToken) -> %% given Serialized = ?TESTED:serialize(ExampleToken), %% when - Result = ?TESTED:authenticate(Serialized), + Result = ?TESTED:authenticate(host_type(), Serialized), %% then ?ae(true, is_validation_success(Result)). @@ -139,26 +139,25 @@ validation_property(_) -> validity_period_test(_) -> %% given - ok = ?TESTED:start(<<"localhost">>, + ok = ?TESTED:start(host_type(), validity_period_cfg(access, {13, hours})), UTCSeconds = utc_now_as_seconds(), ExpectedSeconds = UTCSeconds + ( 13 %% hours * 3600 %% seconds per hour ), %% when - ActualDT = ?TESTED:expiry_datetime(<<"localhost">>, access, UTCSeconds), + ActualDT = ?TESTED:expiry_datetime(host_type(), access, UTCSeconds), %% then ?ae(calendar:gregorian_seconds_to_datetime(ExpectedSeconds), ActualDT). choose_key_by_token_type(_) -> %% given mocked keystore (see init_per_testcase) - JID = jid:from_binary(<<"alice@localhost">>), %% when mod_auth_token asks for key for given token type %% then the correct key is returned - ?ae(<<"access_or_refresh">>, ?TESTED:get_key_for_user(access, JID)), - ?ae(<<"access_or_refresh">>, ?TESTED:get_key_for_user(refresh, JID)), - ?ae(<<"provision">>, ?TESTED:get_key_for_user(provision, JID)). + ?ae(<<"access_or_refresh">>, ?TESTED:get_key_for_host_type(host_type(), access)), + ?ae(<<"access_or_refresh">>, ?TESTED:get_key_for_host_type(host_type(), refresh)), + ?ae(<<"provision">>, ?TESTED:get_key_for_host_type(host_type(), provision)). is_join_and_split_with_base16_and_zeros_reversible(RawToken) -> MAC = base16:encode(crypto:mac(hmac, sha384, <<"unused_key">>, RawToken)), @@ -177,7 +176,7 @@ is_serialization_reversible(Token) -> is_valid_token_prop(Token) -> Serialized = ?TESTED:serialize(Token), - R = ?TESTED:authenticate(Serialized), + R = ?TESTED:authenticate(host_type(), Serialized), case is_validation_success(R) of true -> true; _ -> ct:fail(R) @@ -199,9 +198,9 @@ revoked_token_is_not_valid(_) -> expiry_datetime = ?TESTED:seconds_to_datetime(utc_now_as_seconds() + 10), user_jid = jid:from_binary(<<"alice@localhost">>), sequence_no = RevokedSeqNo}, - Revoked = ?TESTED:serialize(?TESTED:token_with_mac(T)), + Revoked = ?TESTED:serialize(?TESTED:token_with_mac(host_type(), T)), %% when - ValidationResult = ?TESTED:authenticate(Revoked), + ValidationResult = ?TESTED:authenticate(host_type(), Revoked), %% then {error, _} = ValidationResult. @@ -225,7 +224,7 @@ utc_now_as_seconds() -> %% ]}. validity_period_cfg(Type, Period) -> Opts = [ {{validity_period, Type}, Period} ], - ets:insert(ejabberd_modules, {ejabberd_module, {?TESTED, <<"localhost">>}, Opts}), + ets:insert(ejabberd_modules, {ejabberd_module, {?TESTED, host_type()}, Opts}), Opts. %% This is a negative test case helper - that's why we invert the logic below. @@ -244,16 +243,16 @@ mock_rdbms_backend() -> meck:new(mod_auth_token_rdbms, []), meck:expect(mod_auth_token_rdbms, start, fun(_) -> ok end), meck:expect(mod_auth_token_rdbms, get_valid_sequence_number, - fun (_) -> valid_seq_no_threshold() end), + fun (_, _) -> valid_seq_no_threshold() end), gen_mod:start_backend_module(?TESTED, [{backend, rdbms}]), ok. mock_keystore() -> - ejabberd_hooks:add(get_key, <<"localhost">>, ?MODULE, mod_keystore_get_key, 50). + ejabberd_hooks:add(get_key, host_type(), ?MODULE, mod_keystore_get_key, 50). mock_gen_iq_handler() -> meck:new(gen_iq_handler, []), - meck:expect(gen_iq_handler, add_iq_handler, fun (_, _, _, _, _, _) -> ok end). + meck:expect(gen_iq_handler, add_iq_handler_for_domain, fun (_, _, _, _, _, _) -> ok end). mod_keystore_get_key(_, {KeyName, _} = KeyID) -> case KeyName of @@ -264,7 +263,7 @@ mod_keystore_get_key(_, {KeyName, _} = KeyID) -> mock_tested_backend() -> meck:new(mod_auth_token_rdbms, []), meck:expect(mod_auth_token_rdbms, get_valid_sequence_number, - fun (_) -> + fun (_, _) -> receive {valid_seq_no, SeqNo} -> SeqNo end end). @@ -275,8 +274,8 @@ mock_ejabberd_commands() -> provision_token_example() -> {token,provision, {{2055,10,27},{10,54,22}}, - {jid,<<"cEE2M1S0I">>,<<"localhost">>,<<>>,<<"cee2m1s0i">>, - <<"localhost">>,<<>>}, + {jid,<<"cEE2M1S0I">>,domain(),<<>>,<<"cee2m1s0i">>, + domain(),<<>>}, undefined, {xmlel,<<"vCard">>, [{<<"sgzldnl">>,<<"inxdutpu">>}, @@ -323,7 +322,7 @@ provision_token_example() -> refresh_token_example() -> {token,refresh, {{2055,10,27},{10,54,14}}, - {jid,<<"a">>,<<"localhost">>,<<>>,<<"a">>,<<"localhost">>,<<>>}, + {jid,<<"a">>,domain(),<<>>,<<"a">>,domain(),<<>>}, 4,undefined, <<151,225,117,181,0,168,228,208,238,182,157,253,24,200,231,25,189, 160,176,144,85,193,20,108,31,23,46,35,215,41,250,57,68,201,45,33, @@ -361,11 +360,11 @@ make_token({Type, Expiry, JID, SeqNo, VCard}) -> user_jid = jid:from_binary(JID)}, case Type of access -> - ?TESTED:token_with_mac(T); + ?TESTED:token_with_mac(host_type(), T); refresh -> - ?TESTED:token_with_mac(T#token{sequence_no = SeqNo}); + ?TESTED:token_with_mac(host_type(), T#token{sequence_no = SeqNo}); provision -> - ?TESTED:token_with_mac(T#token{vcard = VCard}) + ?TESTED:token_with_mac(host_type(), T#token{vcard = VCard}) end. serialized_token(Sep) -> @@ -400,15 +399,16 @@ vcard() -> bare_jid() -> ?LET({Username, Domain}, {username(), domain()}, - <<(?l2b(Username))/bytes, "@", (?l2b(Domain))/bytes>>). + <<(?l2b(Username))/bytes, "@", (Domain)/bytes>>). %full_jid() -> % ?LET({Username, Domain, Res}, {username(), domain(), resource()}, % <<(?l2b(Username))/bytes, "@", (?l2b(Domain))/bytes, "/", (?l2b(Res))/bytes>>). username() -> ascii_string(). -domain() -> "localhost". +domain() -> <<"localhost">>. %resource() -> ascii_string(). +host_type() -> <<"localhost">>. ascii_string() -> ?LET({Alpha, Alnum}, {ascii_alpha(), list(ascii_alnum())}, [Alpha | Alnum]).