From f48bb16b7d61d12a51aaa3599e1b33159b6be3cd Mon Sep 17 00:00:00 2001 From: Janusz Jakubiec Date: Wed, 20 Jul 2022 12:49:10 +0200 Subject: [PATCH] Adding graphql resolvers and tests --- big_tests/default.spec | 1 + big_tests/dynamic_domains.spec | 1 + big_tests/tests/graphql_token_SUITE.erl | 193 ++++++++++++++++++ priv/graphql/schemas/admin/admin_schema.gql | 2 + priv/graphql/schemas/admin/token.gql | 9 + priv/graphql/schemas/global/token.gql | 4 + priv/graphql/schemas/user/token.gql | 9 + priv/graphql/schemas/user/user_schema.gql | 2 + .../admin/mongoose_graphql_admin_mutation.erl | 4 +- .../mongoose_graphql_token_admin_mutation.erl | 31 +++ src/graphql/mod_auth_token_api.erl | 29 +-- src/graphql/mongoose_graphql.erl | 2 + .../mongoose_graphql_token_user_mutation.erl | 33 +++ .../user/mongoose_graphql_user_mutation.erl | 4 +- 14 files changed, 309 insertions(+), 15 deletions(-) create mode 100644 big_tests/tests/graphql_token_SUITE.erl create mode 100644 priv/graphql/schemas/admin/token.gql create mode 100644 priv/graphql/schemas/global/token.gql create mode 100644 priv/graphql/schemas/user/token.gql create mode 100644 src/graphql/admin/mongoose_graphql_token_admin_mutation.erl create mode 100644 src/graphql/user/mongoose_graphql_token_user_mutation.erl diff --git a/big_tests/default.spec b/big_tests/default.spec index 89e5b60028..9281b4be0e 100644 --- a/big_tests/default.spec +++ b/big_tests/default.spec @@ -38,6 +38,7 @@ {suites, "tests", graphql_session_SUITE}. {suites, "tests", graphql_stanza_SUITE}. {suites, "tests", graphql_stats_SUITE}. +{suites, "tests", graphql_token_SUITE}. {suites, "tests", graphql_vcard_SUITE}. {suites, "tests", graphql_http_upload_SUITE}. {suites, "tests", graphql_metric_SUITE}. diff --git a/big_tests/dynamic_domains.spec b/big_tests/dynamic_domains.spec index da2fe31b10..3a0485f159 100644 --- a/big_tests/dynamic_domains.spec +++ b/big_tests/dynamic_domains.spec @@ -55,6 +55,7 @@ {suites, "tests", graphql_vcard_SUITE}. {suites, "tests", graphql_offline_SUITE}. {suites, "tests", graphql_stats_SUITE}. +{suites, "tests", graphql_token_SUITE}. {suites, "tests", graphql_http_upload_SUITE}. {suites, "tests", graphql_metric_SUITE}. diff --git a/big_tests/tests/graphql_token_SUITE.erl b/big_tests/tests/graphql_token_SUITE.erl new file mode 100644 index 0000000000..dbf6703c03 --- /dev/null +++ b/big_tests/tests/graphql_token_SUITE.erl @@ -0,0 +1,193 @@ +-module(graphql_token_SUITE). + +-compile([export_all, nowarn_export_all]). + +-import(distributed_helper, [require_rpc_nodes/1]). +-import(graphql_helper, [execute_user/3, execute_auth/2, user_to_bin/1]). + +-include_lib("common_test/include/ct.hrl"). +-include_lib("eunit/include/eunit.hrl"). +-include_lib("exml/include/exml.hrl"). +-include_lib("escalus/include/escalus.hrl"). + +suite() -> + require_rpc_nodes([mim]) ++ escalus:suite(). + +all() -> + [{group, user}, + {group, admin}]. + +groups() -> + [{user, [], user_cases()}, + {admin, [], admin_cases()}]. + +user_cases() -> + [user_request_token, + user_revoke_token_no_token_before, + user_revoke_token]. + +admin_cases() -> + [admin_request_token, + admin_request_token_no_user, + admin_revoke_token_no_user, + admin_revoke_token_no_token, + admin_revoke_token]. + +init_per_suite(Config0) -> + case mongoose_helper:is_rdbms_enabled(domain_helper:host_type()) of + true -> + HostType = domain_helper:host_type(), + Config = dynamic_modules:save_modules(HostType, Config0), + dynamic_modules:ensure_modules(HostType, required_modules()), + escalus:init_per_suite(Config); + false -> + {skip, "RDBMS not available"} + end. + +end_per_suite(Config) -> + dynamic_modules:restore_modules(Config), + escalus:end_per_suite(Config). + +required_modules() -> + KeyOpts = #{keys => #{token_secret => ram, + provision_pre_shared => ram}}, + KeyStoreOpts = config_parser_helper:mod_config(mod_keystore, KeyOpts), + [{mod_keystore, KeyStoreOpts}, + {mod_auth_token, auth_token_opts()}]. + +auth_token_opts() -> + Defaults = config_parser_helper:default_mod_config(mod_auth_token), + Defaults#{validity_period => #{access => #{value => 60, unit => minutes}, + refresh => #{value => 1, unit => days}}}. + +init_per_group(admin, Config) -> + graphql_helper:init_admin_handler(Config); +init_per_group(user, Config) -> + [{schema_endpoint, user} | Config]. + +end_per_group(_, _Config) -> + escalus_fresh:clean(). + +init_per_testcase(CaseName, Config) -> + escalus:init_per_testcase(CaseName, Config). + +end_per_testcase(CaseName, Config) -> + escalus:end_per_testcase(CaseName, Config). + +% User tests + +user_request_token(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], fun user_request_token/2). + +user_request_token(Config, Alice) -> + Req = #{query => user_request_token_mutation(), operationName => <<"M1">>, + variables => #{}}, + Res = execute_user(Req, Alice, Config), + #{<<"refresh">> := Refresh, <<"access">> := Access} = ok_result(<<"token">>, + <<"requestToken">>, Res), + ?assert(is_binary(Refresh)), + ?assert(is_binary(Access)). + +user_revoke_token_no_token_before(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], fun user_revoke_token_no_token_before/2). + +user_revoke_token_no_token_before(Config, Alice) -> + Req = #{query => user_revoke_token_mutation(), operationName => <<"M1">>, + variables => #{}}, + Res = execute_user(Req, Alice, Config), + ParsedRes = error_result(<<"extensions">>, <<"code">>, Res), + ?assertEqual(<<"not_found">>, ParsedRes). + +user_revoke_token(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], fun user_revoke_token/2). + +user_revoke_token(Config, Alice) -> + Req = #{query => user_request_token_mutation(), operationName => <<"M1">>, + variables => #{}}, + execute_user(Req, Alice, Config), + Req2 = #{query => user_revoke_token_mutation(), operationName => <<"M1">>, + variables => #{}}, + Res2 = execute_user(Req2, Alice, Config), + ParsedRes = ok_result(<<"token">>, <<"revokeToken">>, Res2), + ?assertEqual(<<"Revoked">>, ParsedRes). + +% Admin tests + +admin_request_token(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], fun admin_request_token/2). + +admin_request_token(Config, Alice) -> + Req = #{query => admin_request_token_mutation(), operationName => <<"M1">>, + variables => #{<<"user">> => user_to_bin(Alice)}}, + Res = execute_auth(Req, Config), + #{<<"refresh">> := Refresh, <<"access">> := Access} = ok_result(<<"token">>, + <<"requestToken">>, Res), + ?assert(is_binary(Refresh)), + ?assert(is_binary(Access)). + +admin_request_token_no_user(Config) -> + Req = #{query => admin_request_token_mutation(), operationName => <<"M1">>, + variables => #{<<"user">> => <<"AAAA">>}}, + Res = execute_auth(Req, Config), + ParsedRes = error_result(<<"extensions">>, <<"code">>, Res), + ?assertEqual(<<"not_found">>, ParsedRes). + +admin_revoke_token_no_user(Config) -> + Req = #{query => admin_revoke_token_mutation(), operationName => <<"M1">>, + variables => #{<<"user">> => <<"AAAA">>}}, + Res = execute_auth(Req, Config), + ParsedRes = error_result(<<"extensions">>, <<"code">>, Res), + ?assertEqual(<<"not_found">>, ParsedRes). + +admin_revoke_token_no_token(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], fun admin_revoke_token_no_token/2). + +admin_revoke_token_no_token(Config, Alice) -> + Req = #{query => admin_revoke_token_mutation(), operationName => <<"M1">>, + variables => #{<<"user">> => user_to_bin(Alice)}}, + Res = execute_auth(Req, Config), + ParsedRes = error_result(<<"extensions">>, <<"code">>, Res), + ?assertEqual(<<"not_found">>, ParsedRes). + +admin_revoke_token(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], fun admin_revoke_token/2). + +admin_revoke_token(Config, Alice) -> + Req = #{query => admin_request_token_mutation(), operationName => <<"M1">>, + variables => #{<<"user">> => user_to_bin(Alice)}}, + execute_auth(Req, Config), + Req2 = #{query => admin_revoke_token_mutation(), operationName => <<"M1">>, + variables => #{<<"user">> => user_to_bin(Alice)}}, + Res2 = execute_auth(Req2, Config), + ParsedRes = ok_result(<<"token">>, <<"revokeToken">>, Res2), + ?assertEqual(<<"Revoked">>, ParsedRes). + +user_request_token_mutation() -> + <<"mutation M1 + { token { + requestToken {access refresh} + } }">>. + +user_revoke_token_mutation() -> + <<"mutation M1 + { token { + revokeToken + } }">>. + +admin_request_token_mutation() -> + <<"mutation M1($user: JID!) + { token { + requestToken(user: $user) {access refresh} + } }">>. + +admin_revoke_token_mutation() -> + <<"mutation M1($user: JID!) + { token { + revokeToken(user: $user) + } }">>. + +ok_result(What1, What2, {{<<"200">>, <<"OK">>}, #{<<"data">> := Data}}) -> + maps:get(What2, maps:get(What1, Data)). + +error_result(What1, What2, {{<<"200">>, <<"OK">>}, #{<<"errors">> := [Data]}}) -> + maps:get(What2, maps:get(What1, Data)). diff --git a/priv/graphql/schemas/admin/admin_schema.gql b/priv/graphql/schemas/admin/admin_schema.gql index 8d6d42bf4a..7517f5f4b6 100644 --- a/priv/graphql/schemas/admin/admin_schema.gql +++ b/priv/graphql/schemas/admin/admin_schema.gql @@ -67,4 +67,6 @@ type AdminMutation @protected{ httpUpload: HttpUploadAdminMutation "Offline deleting old messages" offline: OfflineAdminMutation + "OAUTH token management" + token: TokenAdminMutation } diff --git a/priv/graphql/schemas/admin/token.gql b/priv/graphql/schemas/admin/token.gql new file mode 100644 index 0000000000..f90694481a --- /dev/null +++ b/priv/graphql/schemas/admin/token.gql @@ -0,0 +1,9 @@ +""" + Allow admin to get and revoke user's auth tokens + """ + type TokenAdminMutation @protected { + "Request auth token for an user" + requestToken(user: JID!): Token + "Revoke any tokens for an user" + revokeToken(user: JID!): String + } diff --git a/priv/graphql/schemas/global/token.gql b/priv/graphql/schemas/global/token.gql new file mode 100644 index 0000000000..b2af0f7f7a --- /dev/null +++ b/priv/graphql/schemas/global/token.gql @@ -0,0 +1,4 @@ +type Token { + access: String + refresh: String + } diff --git a/priv/graphql/schemas/user/token.gql b/priv/graphql/schemas/user/token.gql new file mode 100644 index 0000000000..f8beed436e --- /dev/null +++ b/priv/graphql/schemas/user/token.gql @@ -0,0 +1,9 @@ +""" + Allow user to get and revoke tokens. + """ + type TokenUserMutation @protected{ + "Get a new token" + requestToken: Token + "Revoke any tokens" + revokeToken: String + } diff --git a/priv/graphql/schemas/user/user_schema.gql b/priv/graphql/schemas/user/user_schema.gql index 4593ff72df..cbda662808 100644 --- a/priv/graphql/schemas/user/user_schema.gql +++ b/priv/graphql/schemas/user/user_schema.gql @@ -55,4 +55,6 @@ type UserMutation @protected{ private: PrivateUserMutation "Http upload" httpUpload: HttpUploadUserMutation + "OAUTH token management" + token: TokenUserMutation } diff --git a/src/graphql/admin/mongoose_graphql_admin_mutation.erl b/src/graphql/admin/mongoose_graphql_admin_mutation.erl index 7288be1646..10d0f8235f 100644 --- a/src/graphql/admin/mongoose_graphql_admin_mutation.erl +++ b/src/graphql/admin/mongoose_graphql_admin_mutation.erl @@ -30,4 +30,6 @@ execute(_Ctx, _Obj, <<"session">>, _Args) -> execute(_Ctx, _Obj, <<"stanza">>, _Args) -> {ok, stanza}; execute(_Ctx, _Obj, <<"vcard">>, _Args) -> - {ok, vcard}. + {ok, vcard}; +execute(_Ctx, _Obj, <<"token">>, _Args) -> + {ok, token}. diff --git a/src/graphql/admin/mongoose_graphql_token_admin_mutation.erl b/src/graphql/admin/mongoose_graphql_token_admin_mutation.erl new file mode 100644 index 0000000000..a72a8dce01 --- /dev/null +++ b/src/graphql/admin/mongoose_graphql_token_admin_mutation.erl @@ -0,0 +1,31 @@ +-module(mongoose_graphql_token_admin_mutation). + -behaviour(mongoose_graphql). + + -export([execute/4]). + + -ignore_xref([execute/4]). + + -include("../mongoose_graphql_types.hrl"). + + -import(mongoose_graphql_helper, [make_error/2, format_result/2]). + + -type token_info() :: map(). + + execute(_Ctx, token, <<"requestToken">>, #{<<"user">> := JID}) -> + request_token(JID); + execute(_Ctx, token, <<"revokeToken">>, #{<<"user">> := JID}) -> + revoke_token(JID). + + -spec request_token(jid:jid()) -> {ok, token_info()} | {error, resolver_error()}. + request_token(JID) -> + case mod_auth_token_api:create_token(JID) of + {ok, _} = Result -> Result; + Error -> make_error(Error, #{user => JID}) + end. + + -spec revoke_token(jid:jid()) -> {ok, binary()} | {error, resolver_error()}. + revoke_token(JID) -> + case mod_auth_token_api:revoke_token_command(JID) of + {ok, _} = Result -> Result; + Error -> make_error(Error, #{user => JID}) + end. diff --git a/src/graphql/mod_auth_token_api.erl b/src/graphql/mod_auth_token_api.erl index 382c19ae5d..af14ff0b95 100644 --- a/src/graphql/mod_auth_token_api.erl +++ b/src/graphql/mod_auth_token_api.erl @@ -3,11 +3,13 @@ -include("jlib.hrl"). -include("mod_auth_token.hrl"). --export([revoke_token_command/1, create_token/2]). +-export([revoke_token_command/1, create_token/1]). + +-import(mod_auth_token, [token/3, serialize/1]). -spec revoke_token_command(User) -> ResTuple when User :: binary(), - ResCode :: ok | not_found | error, + ResCode :: ok | not_found | internal_server_error, ResTuple :: {ResCode, string()}. revoke_token_command(User) -> #jid{lserver = LServer} = Jid = convert_user(User), @@ -15,33 +17,34 @@ revoke_token_command(User) -> {ok, HostType} -> try mod_auth_token:revoke(HostType, Jid) of not_found -> - {not_found, "User or token not found."}; + {not_found, "User or token not found"}; ok -> - {ok, "Revoked."}; + {ok, "Revoked"}; error -> - {error, "Internal server error"} + {internal_server_error, "Internal server error"} catch _:_ -> - {error, "Internal server error"} + {internal_server_error, "Internal server error"} end; _ -> - {not_found, "User or token not found."} + {not_found, "User or token not found"} end. --spec create_token(User, Type) -> ResTuple when +-spec create_token(User) -> ResTuple when User :: jid:jid(), - Type :: access | refresh, ResCode :: ok | internal_server_error | not_found, ResTuple :: {ResCode, string()}. -create_token(User, Type) -> +create_token(User) -> #jid{lserver = LServer} = Jid = convert_user(User), case mongoose_domain_api:get_domain_host_type(LServer) of {ok, HostType} -> - case mod_auth_token:token(HostType, Jid, Type) of - #token{} = Token -> {ok, mod_auth_token:serialize(Token)}; + case {token(HostType, Jid, access), token(HostType, Jid, refresh)} of + {#token{} = AccessToken, #token{} = RefreshToken} -> + {ok, #{<<"access">> => serialize(AccessToken), + <<"refresh">> => serialize(RefreshToken)}}; _ -> {internal_server_error, "Internal server errror"} end; _ -> - {not_found, "User or token not found."} + {not_found, "User or token not found"} end. convert_user(User) when is_binary(User) -> diff --git a/src/graphql/mongoose_graphql.erl b/src/graphql/mongoose_graphql.erl index 2cea470ae2..5131b07226 100644 --- a/src/graphql/mongoose_graphql.erl +++ b/src/graphql/mongoose_graphql.erl @@ -137,6 +137,7 @@ admin_mapping_rules() -> 'SessionAdminQuery' => mongoose_graphql_session_admin_query, 'StanzaAdminMutation' => mongoose_graphql_stanza_admin_mutation, 'StatsAdminQuery' => mongoose_graphql_stats_admin_query, + 'TokenAdminMutation' => mongoose_graphql_token_admin_mutation, 'GlobalStats' => mongoose_graphql_stats_global, 'DomainStats' => mongoose_graphql_stats_domain, 'StanzaAdminQuery' => mongoose_graphql_stanza_admin_query, @@ -185,6 +186,7 @@ user_mapping_rules() -> 'LastUserQuery' => mongoose_graphql_last_user_query, 'SessionUserQuery' => mongoose_graphql_session_user_query, 'StanzaUserMutation' => mongoose_graphql_stanza_user_mutation, + 'TokenUserMutation' => mongoose_graphql_token_user_mutation, 'StanzaUserQuery' => mongoose_graphql_stanza_user_query, 'HttpUploadUserMutation' => mongoose_graphql_http_upload_user_mutation, 'UserAuthInfo' => mongoose_graphql_user_auth_info, diff --git a/src/graphql/user/mongoose_graphql_token_user_mutation.erl b/src/graphql/user/mongoose_graphql_token_user_mutation.erl new file mode 100644 index 0000000000..7c8db0e1cf --- /dev/null +++ b/src/graphql/user/mongoose_graphql_token_user_mutation.erl @@ -0,0 +1,33 @@ +-module(mongoose_graphql_token_user_mutation). + -behaviour(mongoose_graphql). + + -export([execute/4]). + + -ignore_xref([execute/4]). + + -include("../mongoose_graphql_types.hrl"). + + -import(mongoose_graphql_helper, [null_to_default/2, make_error/2]). + + -type token_info() :: map(). + -type args() :: mongoose_graphql:args(). + -type ctx() :: mongoose_graphql:ctx(). + + execute(Ctx, token, <<"requestToken">>, Args) -> + request_token(Ctx, Args); + execute(Ctx, token, <<"revokeToken">>, Args) -> + revoke_token(Ctx, Args). + + -spec request_token(ctx(), args()) -> {ok, token_info()} | {error, resolver_error()}. + request_token(#{user := JID}, #{}) -> + case mod_auth_token_api:create_token(JID) of + {ok, _} = Result -> Result; + Error -> make_error(Error, #{user => JID}) + end. + + -spec revoke_token(ctx(), args()) -> {ok, binary()} | {error, resolver_error()}. + revoke_token(#{user := JID}, #{}) -> + case mod_auth_token_api:revoke_token_command(JID) of + {ok, _} = Result -> Result; + Error -> make_error(Error, #{user => JID}) + end. diff --git a/src/graphql/user/mongoose_graphql_user_mutation.erl b/src/graphql/user/mongoose_graphql_user_mutation.erl index d012b224b0..d78b3ae8c8 100644 --- a/src/graphql/user/mongoose_graphql_user_mutation.erl +++ b/src/graphql/user/mongoose_graphql_user_mutation.erl @@ -24,4 +24,6 @@ execute(_Ctx, _Obj, <<"roster">>, _Args) -> execute(_Ctx, _Obj, <<"stanza">>, _Args) -> {ok, stanza}; execute(_Ctx, _Obj, <<"vcard">>, _Args) -> - {ok, vcard}. + {ok, vcard}; +execute(_Ctx, _Obj, <<"token">>, _Args) -> + {ok, token}.