From b87035f5a64a225f8510d9a16646e442c6a299ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Wojtasik?= Date: Tue, 17 May 2022 12:56:21 +0200 Subject: [PATCH 01/16] Create an API module for mod_last --- src/admin_extra/service_admin_extra_last.erl | 13 ++--- src/mod_last_api.erl | 51 ++++++++++++++++++++ 2 files changed, 56 insertions(+), 8 deletions(-) create mode 100644 src/mod_last_api.erl diff --git a/src/admin_extra/service_admin_extra_last.erl b/src/admin_extra/service_admin_extra_last.erl index 8fb3fd3e9e..97909efcdc 100644 --- a/src/admin_extra/service_admin_extra_last.erl +++ b/src/admin_extra/service_admin_extra_last.erl @@ -63,14 +63,11 @@ commands() -> -spec set_last(jid:user(), jid:server(), _, _) -> {Res, string()} when Res :: ok | user_does_not_exist. set_last(User, Server, Timestamp, Status) -> - JID = jid:make(User, Server, <<>>), - case ejabberd_auth:does_user_exist(JID) of - true -> - {ok, HostType} = mongoose_domain_api:get_host_type(JID#jid.lserver), - mod_last:store_last_info(HostType, JID#jid.luser, JID#jid.lserver, Timestamp, Status), + JID = jid:make_bare(User, Server), + case mod_last_api:set_last(JID, Timestamp, Status) of + {ok, #{timestamp := Timestamp, status := Status}} -> {ok, io_lib:format("Last activity for user ~s is set as ~B with status ~s", [jid:to_binary(JID), Timestamp, Status])}; - false -> - String = io_lib:format("User ~s@~s does not exist", [User, Server]), - {user_does_not_exist, String} + Error -> + Error end. diff --git a/src/mod_last_api.erl b/src/mod_last_api.erl new file mode 100644 index 0000000000..994a64e126 --- /dev/null +++ b/src/mod_last_api.erl @@ -0,0 +1,51 @@ +%% @doc Provide an interface for frontends (like graphql or ctl) to manage last activity. +-module(mod_last_api). + +-export([get_last/1, set_last/3, count_active_users/2]). + +-include("jlib.hrl"). + +-type info() :: #{timestamp := mod_last:timestamp(), status := mod_last:status()}. +-type timestamp() :: mod_last:timestamp(). +-type status() :: mod_last:status(). + +-define(USER_NOT_FOUND_RESULT(User, Server), + {user_does_not_exist, io_lib:format("User ~s@~s does not exist", [User, Server])}). + +-spec set_last(jid:jid(), timestamp(), status()) -> {ok, info()} | {user_does_not_exist, iolist()}. +set_last(#jid{luser = User, lserver = Server} = JID, Timestamp, Status) -> + case ejabberd_auth:does_user_exist(JID) of + true -> + {ok, HostType} = mongoose_domain_api:get_host_type(Server), + ok = mod_last:store_last_info(HostType, User, Server, Timestamp, Status), + {ok, #{timestamp => Timestamp, status => Status}}; + false -> + ?USER_NOT_FOUND_RESULT(User, Server) + end. + +-spec get_last(jid:jid()) -> + {ok, info()} | {last_not_found | user_does_not_exist, iolist()}. +get_last(#jid{luser = User, lserver = Server} = JID) -> + case ejabberd_auth:does_user_exist(JID) of + true -> + {ok, HostType} = mongoose_domain_api:get_host_type(Server), + case mod_last:get_last_info(HostType, User, Server) of + {ok, Timestamp, Status} -> + {ok, #{timestamp => Timestamp, status => Status}}; + not_found -> + {last_not_found, "Given user's last info not found"} + end; + false -> + ?USER_NOT_FOUND_RESULT(User, Server) + end. + +-spec count_active_users(jid:server(), timestamp()) -> + {ok, pos_integer()} |{domain_not_found, iolist()}. +count_active_users(Domain, Timestamp) -> + LDomain = jid:nameprep(Domain), + case mongoose_domain_api:get_host_type(LDomain) of + {ok, HostType} -> + {ok, mod_last:count_active_users(HostType, LDomain, Timestamp)}; + {error, not_found} -> + {domain_not_found, "Domain not found"} + end. From 44b652adf6070be8d448ad9ccd9b6f5be72a8a4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Wojtasik?= Date: Tue, 17 May 2022 13:00:30 +0200 Subject: [PATCH 02/16] Fix formatting in mod_last --- src/mod_last.erl | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/mod_last.erl b/src/mod_last.erl index 223d8f59d6..5db9a2c3d0 100644 --- a/src/mod_last.erl +++ b/src/mod_last.erl @@ -224,7 +224,7 @@ make_response(HostType, IQ, SubEl, JID, allow) -> end. -spec get_last_info(mongooseim:host_type(), jid:luser(), jid:lserver()) - -> 'not_found' | {'ok', integer(), binary()}. + -> 'not_found' | {'ok', timestamp(), status()}. get_last_info(HostType, LUser, LServer) -> case get_last(HostType, LUser, LServer) of {error, _Reason} -> not_found; @@ -240,7 +240,8 @@ remove_user(Acc, User, Server) -> mongoose_lib:log_if_backend_error(R, ?MODULE, ?LINE, {Acc, User, Server}), Acc. --spec remove_domain(mongoose_hooks:simple_acc(), mongooseim:host_type(), jid:lserver()) -> mongoose_hooks:simple_acc(). +-spec remove_domain(mongoose_hooks:simple_acc(), mongooseim:host_type(), jid:lserver()) -> + mongoose_hooks:simple_acc(). remove_domain(Acc, HostType, Domain) -> mod_last_backend:remove_domain(HostType, Domain), Acc. @@ -263,7 +264,8 @@ store_last_info(Acc, LUser, LServer, Status) -> store_last_info(HostType, LUser, LServer, TimeStamp, Status), Acc. --spec store_last_info(mongooseim:host_type(), jid:luser(), jid:lserver(), timestamp(), status()) -> ok. +-spec store_last_info(mongooseim:host_type(), jid:luser(), jid:lserver(), + timestamp(), status()) -> ok. store_last_info(HostType, LUser, LServer, TimeStamp, Status) -> case mod_last_backend:set_last_info(HostType, LUser, LServer, TimeStamp, Status) of {error, Reason} -> From 68dcb67239210f3b85ce3e70caed428b8046e254 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Wojtasik?= Date: Thu, 19 May 2022 12:49:37 +0200 Subject: [PATCH 03/16] Add the last activity queries to the admin schema --- priv/graphql/schemas/admin/admin_schema.gql | 24 ++++++++++++--------- priv/graphql/schemas/admin/last.gql | 20 +++++++++++++++++ priv/graphql/schemas/global/last.gql | 5 +++++ 3 files changed, 39 insertions(+), 10 deletions(-) create mode 100644 priv/graphql/schemas/admin/last.gql create mode 100644 priv/graphql/schemas/global/last.gql diff --git a/priv/graphql/schemas/admin/admin_schema.gql b/priv/graphql/schemas/admin/admin_schema.gql index 97f156d56a..29d0934b3a 100644 --- a/priv/graphql/schemas/admin/admin_schema.gql +++ b/priv/graphql/schemas/admin/admin_schema.gql @@ -10,18 +10,20 @@ Only an authenticated admin can execute these queries. type AdminQuery{ "Check authorization status" checkAuth: AdminAuthInfo - "Domain management" - domains: DomainAdminQuery "Account management" account: AccountAdminQuery - "Session management" - session: SessionAdminQuery - "Stanza management" - stanza: StanzaAdminQuery + "Domain management" + domains: DomainAdminQuery + "Last activity management" + last: LastAdminQuery "MUC room management" muc: MUCAdminQuery "MUC Light room management" muc_light: MUCLightAdminQuery + "Session management" + session: SessionAdminQuery + "Stanza management" + stanza: StanzaAdminQuery "Roster/Contacts management" roster: RosterAdminQuery "Vcard management" @@ -39,14 +41,16 @@ type AdminMutation @protected{ account: AccountAdminMutation "Domain management" domains: DomainAdminMutation - "Session management" - session: SessionAdminMutation - "Stanza management" - stanza: StanzaAdminMutation + "Last activity management" + last: LastAdminMutation "MUC room management" muc: MUCAdminMutation "MUC Light room management" muc_light: MUCLightAdminMutation + "Session management" + session: SessionAdminMutation + "Stanza management" + stanza: StanzaAdminMutation "Roster/Contacts management" roster: RosterAdminMutation "Vcard management" diff --git a/priv/graphql/schemas/admin/last.gql b/priv/graphql/schemas/admin/last.gql new file mode 100644 index 0000000000..7b8b432e67 --- /dev/null +++ b/priv/graphql/schemas/admin/last.gql @@ -0,0 +1,20 @@ +""" +Allow admin to manage last activity. +""" +type LastAdminQuery @protected{ + "Get the user's last activity information" + getLast(user: JID!): LastActivity + @protected(type: DOMAIN, args: ["user"]) + "Get the number of users active from the given timestamp" + countActiveUsers(domain: String!, timestamp: DateTime): Int + @protected(type: DOMAIN, args: ["domain"]) +} + +""" +Allow admin to get information about last activity. +""" +type LastAdminMutation @protected{ + "Set user's last activity information" + setLast(user: JID!, timestamp: DateTime, status: String!): LastActivity + @protected(type: DOMAIN, args: ["user"]) +} diff --git a/priv/graphql/schemas/global/last.gql b/priv/graphql/schemas/global/last.gql new file mode 100644 index 0000000000..5774e222ef --- /dev/null +++ b/priv/graphql/schemas/global/last.gql @@ -0,0 +1,5 @@ +type LastActivity{ + user: JID! + timestamp: DateTime! + status: String! +} From 51aa580c026b575832380a55eab624093eb5d3dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Wojtasik?= Date: Tue, 17 May 2022 13:03:57 +0200 Subject: [PATCH 04/16] Add the last activities admin resolvers --- .../admin/mongoose_graphql_admin_mutation.erl | 14 +++--- .../admin/mongoose_graphql_admin_query.erl | 16 ++++--- .../mongoose_graphql_last_admin_mutation.erl | 20 +++++++++ .../mongoose_graphql_last_admin_query.erl | 29 ++++++++++++ src/graphql/mongoose_graphql.erl | 2 + src/graphql/mongoose_graphql_last_helper.erl | 44 +++++++++++++++++++ 6 files changed, 112 insertions(+), 13 deletions(-) create mode 100644 src/graphql/admin/mongoose_graphql_last_admin_mutation.erl create mode 100644 src/graphql/admin/mongoose_graphql_last_admin_query.erl create mode 100644 src/graphql/mongoose_graphql_last_helper.erl diff --git a/src/graphql/admin/mongoose_graphql_admin_mutation.erl b/src/graphql/admin/mongoose_graphql_admin_mutation.erl index 596d5a0dde..c38ce086d7 100644 --- a/src/graphql/admin/mongoose_graphql_admin_mutation.erl +++ b/src/graphql/admin/mongoose_graphql_admin_mutation.erl @@ -7,21 +7,23 @@ -include("../mongoose_graphql_types.hrl"). -execute(_Ctx, _Obj, <<"domains">>, _Args) -> - {ok, admin}; execute(_Ctx, _Obj, <<"account">>, _Args) -> {ok, account}; +execute(_Ctx, _Obj, <<"domains">>, _Args) -> + {ok, admin}; +execute(_Ctx, _Obj, <<"last">>, _Args) -> + {ok, last}; execute(_Ctx, _Obj, <<"muc">>, _Args) -> {ok, muc}; execute(_Ctx, _Obj, <<"muc_light">>, _Args) -> {ok, muc_light}; -execute(_Ctx, _Obj, <<"session">>, _Args) -> - {ok, session}; -execute(_Ctx, _Obj, <<"stanza">>, _Args) -> - {ok, stanza}; execute(_Ctx, _Obj, <<"private">>, _Args) -> {ok, private}; execute(_Ctx, _Obj, <<"roster">>, _Args) -> {ok, roster}; +execute(_Ctx, _Obj, <<"session">>, _Args) -> + {ok, session}; +execute(_Ctx, _Obj, <<"stanza">>, _Args) -> + {ok, stanza}; execute(_Ctx, _Obj, <<"vcard">>, _Args) -> {ok, vcard}. diff --git a/src/graphql/admin/mongoose_graphql_admin_query.erl b/src/graphql/admin/mongoose_graphql_admin_query.erl index 18682741c5..4543b86c5b 100644 --- a/src/graphql/admin/mongoose_graphql_admin_query.erl +++ b/src/graphql/admin/mongoose_graphql_admin_query.erl @@ -7,23 +7,25 @@ -include("../mongoose_graphql_types.hrl"). -execute(_Ctx, _Obj, <<"domains">>, _Args) -> +execute(_Ctx, _Obj, <<"checkAuth">>, _Args) -> {ok, admin}; execute(_Ctx, _Obj, <<"account">>, _Args) -> {ok, account}; +execute(_Ctx, _Obj, <<"domains">>, _Args) -> + {ok, admin}; +execute(_Ctx, _Obj, <<"last">>, _Args) -> + {ok, last}; execute(_Ctx, _Obj, <<"muc">>, _Args) -> {ok, muc}; execute(_Ctx, _Obj, <<"muc_light">>, _Args) -> {ok, muc_light}; -execute(_Ctx, _Obj, <<"session">>, _Args) -> - {ok, session}; -execute(_Ctx, _Obj, <<"stanza">>, _Args) -> - {ok, #{}}; execute(_Ctx, _Obj, <<"private">>, _Args) -> {ok, private}; execute(_Ctx, _Obj, <<"roster">>, _Args) -> {ok, roster}; -execute(_Ctx, _Obj, <<"checkAuth">>, _Args) -> - {ok, admin}; +execute(_Ctx, _Obj, <<"session">>, _Args) -> + {ok, session}; +execute(_Ctx, _Obj, <<"stanza">>, _Args) -> + {ok, #{}}; execute(_Ctx, _Obj, <<"vcard">>, _Args) -> {ok, vcard}. diff --git a/src/graphql/admin/mongoose_graphql_last_admin_mutation.erl b/src/graphql/admin/mongoose_graphql_last_admin_mutation.erl new file mode 100644 index 0000000000..37e4767192 --- /dev/null +++ b/src/graphql/admin/mongoose_graphql_last_admin_mutation.erl @@ -0,0 +1,20 @@ +-module(mongoose_graphql_last_admin_mutation). +-behaviour(mongoose_graphql). + +-export([execute/4]). + +-ignore_xref([execute/4]). + +-include("../mongoose_graphql_types.hrl"). + +-import(mongoose_graphql_helper, [make_error/2]). + +-type last_info() :: mongoose_graphql_last_helper:last_info(). +-type args() :: mongoose_graphql:args(). + +execute(_Ctx, last, <<"setLast">>, Args) -> + set_last(Args). + +-spec set_last(args()) -> {ok, last_info()} | {error, resolver_error()}. +set_last(#{<<"user">> := JID, <<"timestamp">> := Timestamp, <<"status">> := Status}) -> + mongoose_graphql_last_helper:set_last(JID, Timestamp, Status). diff --git a/src/graphql/admin/mongoose_graphql_last_admin_query.erl b/src/graphql/admin/mongoose_graphql_last_admin_query.erl new file mode 100644 index 0000000000..3362179f6a --- /dev/null +++ b/src/graphql/admin/mongoose_graphql_last_admin_query.erl @@ -0,0 +1,29 @@ +-module(mongoose_graphql_last_admin_query). +-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, null_to_default/2]). + +-type last_info() :: mongoose_graphql_last_helper:last_info(). +-type args() :: mongoose_graphql:args(). + +execute(_Ctx, last, <<"getLast">>, Args) -> + get_last(Args); +execute(_Ctx, last, <<"countActiveUsers">>, Args) -> + count_active_users(Args). + +-spec get_last(args()) -> {ok, last_info()} | {error, resolver_error()}. +get_last(#{<<"user">> := JID}) -> + mongoose_graphql_last_helper:get_last(JID). + +-spec count_active_users(args()) -> {ok, pos_integer()} | {error, resolver_error()}. +count_active_users(#{<<"domain">> := Domain, <<"timestamp">> := Timestamp}) -> + DefTimestamp = null_to_default(Timestamp, os:system_time(microsecond)), + TimestampSec = mongoose_graphql_last_helper:microseconds_to_seconds(DefTimestamp), + Res = mod_last_api:count_active_users(Domain, TimestampSec), + format_result(Res, #{domain => Domain}). diff --git a/src/graphql/mongoose_graphql.erl b/src/graphql/mongoose_graphql.erl index 6284e98816..0440a7711f 100644 --- a/src/graphql/mongoose_graphql.erl +++ b/src/graphql/mongoose_graphql.erl @@ -136,6 +136,8 @@ admin_mapping_rules() -> 'SessionAdminQuery' => mongoose_graphql_session_admin_query, 'StanzaAdminMutation' => mongoose_graphql_stanza_admin_mutation, 'StanzaAdminQuery' => mongoose_graphql_stanza_admin_query, + 'LastAdminMutation' => mongoose_graphql_last_admin_mutation, + 'LastAdminQuery' => mongoose_graphql_last_admin_query, 'AccountAdminQuery' => mongoose_graphql_account_admin_query, 'AccountAdminMutation' => mongoose_graphql_account_admin_mutation, 'MUCAdminMutation' => mongoose_graphql_muc_admin_mutation, diff --git a/src/graphql/mongoose_graphql_last_helper.erl b/src/graphql/mongoose_graphql_last_helper.erl new file mode 100644 index 0000000000..6cd4209dd7 --- /dev/null +++ b/src/graphql/mongoose_graphql_last_helper.erl @@ -0,0 +1,44 @@ +-module(mongoose_graphql_last_helper). + +-export([get_last/1, set_last/3]). + +-export([microseconds_to_seconds/1, seconds_to_microseconds/1]). + +-ignore_xref([microseconds_to_seconds/1, seconds_to_microseconds/1]). + +-include("mongoose_graphql_types.hrl"). + +-import(mongoose_graphql_helper, [make_error/2, null_to_default/2]). + +-type last_info() :: map(). +-type timestamp() :: mod_last:timestamp() | null. +-type status() :: mod_last:status(). + +-export_type([last_info/0]). + +-spec get_last(jid:jid()) -> {ok, last_info()} | {error, resolver_error()}. +get_last(JID) -> + case mod_last_api:get_last(JID) of + {ok, #{timestamp := T, status := S}} -> + {ok, #{<<"user">> => JID, <<"timestamp">> => seconds_to_microseconds(T), + <<"status">> => S}}; + Error -> + make_error(Error, #{user => jid:to_binary(JID)}) + end. + +-spec set_last(jid:jid(), timestamp(), status()) -> {ok, last_info()} | {error, resolver_error()}. +set_last(JID, Timestamp, Status) -> + DefTimestamp = microseconds_to_seconds(null_to_default(Timestamp, os:system_time(microsecond))), + case mod_last_api:set_last(JID, DefTimestamp, Status) of + {ok, #{timestamp := DefTimestamp, status := Status}} -> + {ok, #{<<"user">> => JID, <<"timestamp">> => seconds_to_microseconds(DefTimestamp), + <<"status">> => Status}}; + Error -> + make_error(Error, #{user => jid:to_binary(JID)}) + end. + +microseconds_to_seconds(Timestamp) -> + Timestamp div 1000000. + +seconds_to_microseconds(Timestamp) -> + Timestamp * 1000000. From c6e90953ebb3c6595be024f74a12459df4b7a792 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Wojtasik?= Date: Tue, 17 May 2022 13:06:30 +0200 Subject: [PATCH 05/16] Add the last activity queries to the user schema --- priv/graphql/schemas/user/last.gql | 15 +++++++++++++++ priv/graphql/schemas/user/user_schema.gql | 4 ++++ 2 files changed, 19 insertions(+) create mode 100644 priv/graphql/schemas/user/last.gql diff --git a/priv/graphql/schemas/user/last.gql b/priv/graphql/schemas/user/last.gql new file mode 100644 index 0000000000..d23411bb65 --- /dev/null +++ b/priv/graphql/schemas/user/last.gql @@ -0,0 +1,15 @@ +""" +Allow user to manage last activity. +""" +type LastUserQuery @protected{ + "Get the user's last activity information" + getLast(user: JID): LastActivity +} + +""" +Allow user to get information about last activity. +""" +type LastUserMutation @protected{ + "Set user's last activity information" + setLast(timestamp: DateTime, status: String!): LastActivity +} diff --git a/priv/graphql/schemas/user/user_schema.gql b/priv/graphql/schemas/user/user_schema.gql index 36d4f8d7d7..4838d693d5 100644 --- a/priv/graphql/schemas/user/user_schema.gql +++ b/priv/graphql/schemas/user/user_schema.gql @@ -12,6 +12,8 @@ type UserQuery{ checkAuth: UserAuthInfo "Account management" account: AccountUserQuery + "Last activity management" + last: LastUserQuery "MUC room management" muc: MUCUserQuery "MUC Light room management" @@ -35,6 +37,8 @@ Only an authenticated user can execute these mutations. type UserMutation @protected{ "Account management" account: AccountUserMutation + "Last activity management" + last: LastUserMutation "MUC room management" muc: MUCUserMutation "MUC Light room management" From db41b4eca06bd9cec63a1f46caec194fc155ec0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Wojtasik?= Date: Tue, 17 May 2022 13:16:24 +0200 Subject: [PATCH 06/16] Add the last activities user resolvers --- src/graphql/mongoose_graphql.erl | 2 ++ .../mongoose_graphql_last_user_mutation.erl | 21 +++++++++++++++++++ .../user/mongoose_graphql_last_user_query.erl | 21 +++++++++++++++++++ .../user/mongoose_graphql_user_mutation.erl | 6 ++++-- .../user/mongoose_graphql_user_query.erl | 14 +++++++------ 5 files changed, 56 insertions(+), 8 deletions(-) create mode 100644 src/graphql/user/mongoose_graphql_last_user_mutation.erl create mode 100644 src/graphql/user/mongoose_graphql_last_user_query.erl diff --git a/src/graphql/mongoose_graphql.erl b/src/graphql/mongoose_graphql.erl index 0440a7711f..a2678719dd 100644 --- a/src/graphql/mongoose_graphql.erl +++ b/src/graphql/mongoose_graphql.erl @@ -173,6 +173,8 @@ user_mapping_rules() -> 'RosterUserMutation' => mongoose_graphql_roster_user_mutation, 'VcardUserMutation' => mongoose_graphql_vcard_user_mutation, 'VcardUserQuery' => mongoose_graphql_vcard_user_query, + 'LastUserMutation' => mongoose_graphql_last_user_mutation, + 'LastUserQuery' => mongoose_graphql_last_user_query, 'SessionUserQuery' => mongoose_graphql_session_user_query, 'StanzaUserMutation' => mongoose_graphql_stanza_user_mutation, 'StanzaUserQuery' => mongoose_graphql_stanza_user_query, diff --git a/src/graphql/user/mongoose_graphql_last_user_mutation.erl b/src/graphql/user/mongoose_graphql_last_user_mutation.erl new file mode 100644 index 0000000000..21e082943d --- /dev/null +++ b/src/graphql/user/mongoose_graphql_last_user_mutation.erl @@ -0,0 +1,21 @@ +-module(mongoose_graphql_last_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]). + +-type last_info() :: map(). +-type args() :: mongoose_graphql:args(). +-type ctx() :: mongoose_graphql:ctx(). + +execute(Ctx, last, <<"setLast">>, Args) -> + set_last(Ctx, Args). + +-spec set_last(ctx(), args()) -> {ok, last_info()} | {error, resolver_error()}. +set_last(#{user := JID}, #{<<"timestamp">> := Timestamp, <<"status">> := Status}) -> + mongoose_graphql_last_helper:set_last(JID, Timestamp, Status). diff --git a/src/graphql/user/mongoose_graphql_last_user_query.erl b/src/graphql/user/mongoose_graphql_last_user_query.erl new file mode 100644 index 0000000000..d3655e075d --- /dev/null +++ b/src/graphql/user/mongoose_graphql_last_user_query.erl @@ -0,0 +1,21 @@ +-module(mongoose_graphql_last_user_query). +-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, null_to_default/2]). + +-type last_info() :: mongoose_graphql_last_helper:last_info(). +-type args() :: mongoose_graphql:args(). +-type ctx() :: mongoose_graphql:ctx(). + +execute(Ctx, last, <<"getLast">>, Args) -> + get_last(Ctx, Args). + +-spec get_last(ctx(), args()) -> {ok, last_info()} | {error, resolver_error()}. +get_last(#{user := UserJID}, #{<<"user">> := JID}) -> + mongoose_graphql_last_helper:get_last(null_to_default(JID, UserJID)). diff --git a/src/graphql/user/mongoose_graphql_user_mutation.erl b/src/graphql/user/mongoose_graphql_user_mutation.erl index dbf6878bf5..19a001dcf5 100644 --- a/src/graphql/user/mongoose_graphql_user_mutation.erl +++ b/src/graphql/user/mongoose_graphql_user_mutation.erl @@ -7,15 +7,17 @@ execute(_Ctx, _Obj, <<"account">>, _Args) -> {ok, account}; +execute(_Ctx, _Obj, <<"last">>, _Args) -> + {ok, last}; execute(_Ctx, _Obj, <<"muc">>, _Args) -> {ok, muc}; execute(_Ctx, _Obj, <<"muc_light">>, _Args) -> {ok, muc_light}; -execute(_Ctx, _Obj, <<"stanza">>, _Args) -> - {ok, stanza}; execute(_Ctx, _Obj, <<"private">>, _Args) -> {ok, private}; execute(_Ctx, _Obj, <<"roster">>, _Args) -> {ok, roster}; +execute(_Ctx, _Obj, <<"stanza">>, _Args) -> + {ok, stanza}; execute(_Ctx, _Obj, <<"vcard">>, _Args) -> {ok, vcard}. diff --git a/src/graphql/user/mongoose_graphql_user_query.erl b/src/graphql/user/mongoose_graphql_user_query.erl index 1ab3b8171f..79c1480539 100644 --- a/src/graphql/user/mongoose_graphql_user_query.erl +++ b/src/graphql/user/mongoose_graphql_user_query.erl @@ -5,21 +5,23 @@ -ignore_xref([execute/4]). +execute(_Ctx, _Obj, <<"checkAuth">>, _Args) -> + {ok, user}; execute(_Ctx, _Obj, <<"account">>, _Args) -> {ok, account}; +execute(_Ctx, _Obj, <<"last">>, _Args) -> + {ok, last}; execute(_Ctx, _Obj, <<"muc">>, _Args) -> {ok, muc}; execute(_Ctx, _Obj, <<"muc_light">>, _Args) -> {ok, muc_light}; -execute(_Ctx, _Obj, <<"session">>, _Args) -> - {ok, session}; -execute(_Ctx, _Obj, <<"checkAuth">>, _Args) -> - {ok, user}; -execute(_Ctx, _Obj, <<"stanza">>, _Args) -> - {ok, stanza}; execute(_Ctx, _Obj, <<"private">>, _Args) -> {ok, private}; execute(_Ctx, _Obj, <<"roster">>, _Args) -> {ok, roster}; +execute(_Ctx, _Obj, <<"session">>, _Args) -> + {ok, session}; +execute(_Ctx, _Obj, <<"stanza">>, _Args) -> + {ok, stanza}; execute(_Ctx, _Obj, <<"vcard">>, _Args) -> {ok, vcard}. From f5c5131a94d9358ababbc801b9201a99a084dbde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Wojtasik?= Date: Tue, 17 May 2022 15:52:10 +0200 Subject: [PATCH 07/16] Add tests for graphql last --- big_tests/tests/graphql_helper.erl | 8 +- big_tests/tests/graphql_last_SUITE.erl | 207 +++++++++++++++++++++++++ 2 files changed, 213 insertions(+), 2 deletions(-) create mode 100644 big_tests/tests/graphql_last_SUITE.erl diff --git a/big_tests/tests/graphql_helper.erl b/big_tests/tests/graphql_helper.erl index d425b2f3e7..eeb6191155 100644 --- a/big_tests/tests/graphql_helper.erl +++ b/big_tests/tests/graphql_helper.erl @@ -5,7 +5,7 @@ -export([execute/3, execute_auth/2, execute_domain_auth/2, execute_user/3]). -export([init_admin_handler/1, init_domain_admin_handler/1, end_domain_admin_handler/1]). -export([get_listener_port/1, get_listener_config/1]). --export([get_ok_value/2, get_err_msg/1, get_err_msg/2, make_creds/1, +-export([get_ok_value/2, get_err_msg/1, get_err_msg/2, get_err_code/1, make_creds/1, user_to_bin/1, user_to_full_bin/1, user_to_jid/1, user_to_lower_jid/1]). -include_lib("common_test/include/ct.hrl"). @@ -77,6 +77,9 @@ get_listener_opts(EpName) -> #{handlers := [Opts]} = get_listener_config(EpName), Opts. +get_err_code(Resp) -> + get_ok_value([errors, 1, extensions, code], Resp). + -spec get_err_msg(#{errors := [#{message := binary()}]}) -> binary(). get_err_msg(Resp) -> get_ok_value([errors, 1, message], Resp). @@ -100,7 +103,8 @@ user_to_full_bin(#client{} = Client) -> escalus_client:full_jid(Client); user_to_full_bin(Bin) when is_binary(Bin) -> Bin. user_to_bin(#client{} = Client) -> escalus_client:short_jid(Client); -user_to_bin(Bin) when is_binary(Bin) -> Bin. +user_to_bin(Bin) when is_binary(Bin) -> Bin; +user_to_bin(null) -> null. user_to_jid(#client{jid = JID}) -> jid:to_bare(jid:from_binary(JID)); user_to_jid(Bin) when is_binary(Bin) -> jid:to_bare(jid:from_binary(Bin)). diff --git a/big_tests/tests/graphql_last_SUITE.erl b/big_tests/tests/graphql_last_SUITE.erl new file mode 100644 index 0000000000..95dc8650e1 --- /dev/null +++ b/big_tests/tests/graphql_last_SUITE.erl @@ -0,0 +1,207 @@ +-module(graphql_last_SUITE). + +-compile([export_all, nowarn_export_all]). + +-import(distributed_helper, [mim/0, require_rpc_nodes/1, rpc/4]). +-import(graphql_helper, [execute_user/3, execute_auth/2, user_to_bin/1, + get_ok_value/2, get_err_msg/1, get_err_code/1]). + +-include_lib("eunit/include/eunit.hrl"). + +-define(assertErrMsg(Res, ContainsPart), assert_err_msg(ContainsPart, Res)). +-define(assertErrCode(Res, Code), assert_err_code(Code, Res)). + +-define(NONEXISTENT_JID, <<"user@user.com">>). +-define(DEFAULT_DT, <<"2022-04-17T12:58:30.000000Z">>). + +suite() -> + require_rpc_nodes([mim]) ++ escalus:suite(). + +all() -> + [{group, user_last}, + {group, admin_last}]. + +groups() -> + [{user_last, [parallel], user_last_handler()}, + {admin_last, [parallel], admin_last_handler()}]. + +user_last_handler() -> + [mock]. + +admin_last_handler() -> + [admin_set_last, + admin_try_set_nonexistent_user_last, + admin_get_last, + admin_get_nonexistent_user_last, + admin_try_get_nonexistent_last, + admin_count_active_users, + admin_try_count_nonexistent_domain_active_users]. + +init_per_suite(Config) -> + Config1 = escalus:init_per_suite(Config), + Config2 = dynamic_modules:save_modules(domain_helper:host_type(), Config1), + HostType = domain_helper:host_type(), + Backend = mongoose_helper:get_backend_mnesia_rdbms_riak(HostType), + dynamic_modules:ensure_modules(HostType, required_modules(Backend)), + escalus:init_per_suite([{backend, Backend} | Config2]). + + +end_per_suite(Config) -> + dynamic_modules:restore_modules(Config), + escalus:end_per_suite(Config). + +init_per_group(admin_last, Config) -> + graphql_helper:init_admin_handler(Config); +init_per_group(user_last, Config) -> + [{schema_endpoint, user} | Config]. + +end_per_group(admin_last, _Config) -> + escalus_fresh:clean(); +end_per_group(user_last, _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). + +required_modules(riak) -> + [{mod_last, #{backend => riak, + iqdisc => one_queue, + riak => #{bucket_type => <<"last">>}}}]; +required_modules(Backend) -> + [{mod_last, #{backend => Backend, + iqdisc => one_queue}}]. + +%% Admin test cases + +admin_set_last(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], + fun admin_set_last/2). + +admin_set_last(Config, Alice) -> + Status = <<"First status">>, + JID = escalus_utils:jid_to_lower(user_to_bin(Alice)), + % With timestamp provided + Res = execute_auth(admin_set_last_body(Alice, Status, ?DEFAULT_DT), Config), + #{<<"user">> := JID, <<"status">> := Status, <<"timestamp">> := ?DEFAULT_DT} = + get_ok_value(p(setLast), Res), + % Without timestamp + Status2 = <<"Second status">>, + Res2 = execute_auth(admin_set_last_body(Alice, Status2, null), Config), + #{<<"user">> := JID, <<"status">> := Status2, <<"timestamp">> := DateTime2} = + get_ok_value(p(setLast), Res2), + ?assert(os:system_time(second) - dt_to_unit(DateTime2, second) < 2). + +admin_try_set_nonexistent_user_last(Config) -> + Res = execute_auth(admin_set_last_body(?NONEXISTENT_JID, <<"status">>, null), Config), + ?assertErrMsg(Res, <<"not exist">>), + ?assertErrCode(Res, user_does_not_exist). + +admin_get_last(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], + fun admin_get_last/2). + +admin_get_last(Config, Alice) -> + Status = <<"I love ducks">>, + JID = escalus_utils:jid_to_lower(user_to_bin(Alice)), + execute_auth(admin_set_last_body(Alice, Status, ?DEFAULT_DT), Config), + Res = execute_auth(admin_get_last_body(Alice), Config), + #{<<"user">> := JID, <<"status">> := Status, <<"timestamp">> := ?DEFAULT_DT} = + get_ok_value(p(getLast), Res). + +admin_get_nonexistent_user_last(Config) -> + Res = execute_auth(admin_get_last_body(?NONEXISTENT_JID), Config), + ?assertErrMsg(Res, <<"not exist">>), + ?assertErrCode(Res, user_does_not_exist). + +admin_try_get_nonexistent_last(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], + fun admin_try_get_nonexistent_last/2). + +admin_try_get_nonexistent_last(Config, Alice) -> + Res = execute_auth(admin_get_last_body(Alice), Config), + ?assertErrMsg(Res, <<"not found">>), + ?assertErrCode(Res, last_not_found). + +admin_count_active_users(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}, {bob, 1}], + fun admin_count_active_users/3). + +admin_count_active_users(Config, Alice, Bob) -> + Domain = domain_helper:domain(), + execute_auth(admin_set_last_body(Alice, <<"a">>, now_dt_with_offset(5)), Config), + execute_auth(admin_set_last_body(Bob, <<"b">>, now_dt_with_offset(10)), Config), + Res = execute_auth(admin_count_active_users_body(Domain, null), Config), + ?assertEqual(2, get_ok_value(p(countActiveUsers), Res)), + Res2 = execute_auth(admin_count_active_users_body(Domain, now_dt_with_offset(30)), Config), + ?assertEqual(0, get_ok_value(p(countActiveUsers), Res2)). + +admin_try_count_nonexistent_domain_active_users(Config) -> + Res = execute_auth(admin_count_active_users_body(<<"unknown-domain.com">>, null), Config), + ?assertErrMsg(Res, <<"not found">>), + ?assertErrCode(Res, domain_not_found). + +%% User test cases + +%% Helpers + +assert_err_msg(Contains, Res) -> + ?assertNotEqual(nomatch, binary:match(get_err_msg(Res), Contains)). + +assert_err_code(Code, Res) -> + ?assertEqual(atom_to_binary(Code), get_err_code(Res)). + +p(Cmd) when is_atom(Cmd) -> + [data, last, Cmd]; +p(Path) when is_list(Path) -> + [data, last] ++ Path. + +now_dt_with_offset(SecondsOffset) -> + Seconds = erlang:system_time(second) + SecondsOffset, + list_to_binary(calendar:system_time_to_rfc3339(Seconds, [{unit, second}, {offset, "Z"}])). + +dt_to_unit(ISODateTime, Unit) -> + calendar:rfc3339_to_system_time(binary_to_list(ISODateTime), [{unit, Unit}]). + +%% Request bodies + +admin_set_last_body(User, Status, DateTime) -> + Query = <<"mutation M1($user: JID!, $timestamp: DateTime, $status: String!) + { last { setLast (user: $user, timestamp: $timestamp, status: $status) + { user timestamp status } } }">>, + OpName = <<"M1">>, + Vars = #{user => user_to_bin(User), timestamp => DateTime, status => Status}, + #{query => Query, operationName => OpName, variables => Vars}. + +admin_get_last_body(User) -> + Query = <<"query Q1($user: JID!) + { last { getLast(user: $user) + { user timestamp status } } }">>, + OpName = <<"Q1">>, + Vars = #{user => user_to_bin(User)}, + #{query => Query, operationName => OpName, variables => Vars}. + +admin_count_active_users_body(Domain, Timestamp) -> + Query = <<"query Q1($domain: String!, $timestamp: DateTime) + { last { countActiveUsers(domain: $domain, timestamp: $timestamp) } }">>, + OpName = <<"Q1">>, + Vars = #{domain => Domain, timestamp => Timestamp}, + #{query => Query, operationName => OpName, variables => Vars}. + +user_set_last_body(DateTime, Status) -> + Query = <<"mutation M1($timestamp: DateTime, $status: String!) + { last { setLast (timestamp: $timestamp, status: $status) + { user timestamp status } } }">>, + OpName = <<"M1">>, + Vars = #{timestamp => DateTime, status => Status}, + #{query => Query, operationName => OpName, variables => Vars}. + +user_get_last_body(User) -> + Query = <<"query Q1($user: JID) + { last { getLast(user: $user) + { user timestamp status } } }">>, + OpName = <<"Q1">>, + Vars = #{user => user_to_bin(User)}, + #{query => Query, operationName => OpName, variables => Vars}. From acad84ce8f5f1b4a9ac58a985980e814864ab171 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Wojtasik?= Date: Tue, 17 May 2022 16:09:56 +0200 Subject: [PATCH 08/16] Add tests for graphql user last category --- big_tests/tests/graphql_last_SUITE.erl | 45 ++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/big_tests/tests/graphql_last_SUITE.erl b/big_tests/tests/graphql_last_SUITE.erl index 95dc8650e1..042ced51a1 100644 --- a/big_tests/tests/graphql_last_SUITE.erl +++ b/big_tests/tests/graphql_last_SUITE.erl @@ -26,7 +26,9 @@ groups() -> {admin_last, [parallel], admin_last_handler()}]. user_last_handler() -> - [mock]. + [user_set_last, + user_get_last, + user_get_other_user_last]. admin_last_handler() -> [admin_set_last, @@ -145,6 +147,45 @@ admin_try_count_nonexistent_domain_active_users(Config) -> %% User test cases +user_set_last(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], + fun user_set_last/2). + +user_set_last(Config, Alice) -> + Status = <<"My first status">>, + JID = escalus_utils:jid_to_lower(user_to_bin(Alice)), + Res = execute_user(user_set_last_body(Status, ?DEFAULT_DT), Alice, Config), + #{<<"user">> := JID, <<"status">> := Status, <<"timestamp">> := ?DEFAULT_DT} = + get_ok_value(p(setLast), Res), + Status2 = <<"Quack Quack">>, + Res2 = execute_user(user_set_last_body(Status2, null), Alice, Config), + #{<<"user">> := JID, <<"status">> := Status2, <<"timestamp">> := DateTime2} = + get_ok_value(p(setLast), Res2), + ?assert(os:system_time(second) - dt_to_unit(DateTime2, second) < 2). + +user_get_last(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], + fun user_get_last/2). +user_get_last(Config, Alice) -> + Status = <<"I love ducks">>, + JID = escalus_utils:jid_to_lower(user_to_bin(Alice)), + execute_user(user_set_last_body(Status, ?DEFAULT_DT), Alice, Config), + Res = execute_user(user_get_last_body(null), Alice, Config), + #{<<"user">> := JID, <<"status">> := Status, <<"timestamp">> := ?DEFAULT_DT} = + get_ok_value(p(getLast), Res). + +user_get_other_user_last(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}, {bob, 1}], + fun user_get_other_user_last/3). + +user_get_other_user_last(Config, Alice, Bob) -> + Status = <<"In good mood">>, + JID = escalus_utils:jid_to_lower(user_to_bin(Bob)), + execute_user(user_set_last_body(Status, ?DEFAULT_DT), Bob, Config), + Res = execute_user(admin_get_last_body(Bob), Alice, Config), + #{<<"user">> := JID, <<"status">> := Status, <<"timestamp">> := ?DEFAULT_DT} = + get_ok_value(p(getLast), Res). + %% Helpers assert_err_msg(Contains, Res) -> @@ -190,7 +231,7 @@ admin_count_active_users_body(Domain, Timestamp) -> Vars = #{domain => Domain, timestamp => Timestamp}, #{query => Query, operationName => OpName, variables => Vars}. -user_set_last_body(DateTime, Status) -> +user_set_last_body(Status, DateTime) -> Query = <<"mutation M1($timestamp: DateTime, $status: String!) { last { setLast (timestamp: $timestamp, status: $status) { user timestamp status } } }">>, From af3eccf099a1c1da5aac39e0a52d2c0380fd668c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Wojtasik?= Date: Tue, 17 May 2022 16:31:34 +0200 Subject: [PATCH 09/16] Enable graphql last category big tests --- big_tests/default.spec | 1 + big_tests/dynamic_domains.spec | 1 + 2 files changed, 2 insertions(+) diff --git a/big_tests/default.spec b/big_tests/default.spec index b23aad936f..4aa5e88d22 100644 --- a/big_tests/default.spec +++ b/big_tests/default.spec @@ -28,6 +28,7 @@ {suites, "tests", graphql_SUITE}. {suites, "tests", graphql_account_SUITE}. {suites, "tests", graphql_domain_SUITE}. +{suites, "tests", graphql_last_SUITE}. {suites, "tests", graphql_muc_SUITE}. {suites, "tests", graphql_muc_light_SUITE}. {suites, "tests", graphql_private_SUITE}. diff --git a/big_tests/dynamic_domains.spec b/big_tests/dynamic_domains.spec index 20765dd353..6af3f255a5 100644 --- a/big_tests/dynamic_domains.spec +++ b/big_tests/dynamic_domains.spec @@ -44,6 +44,7 @@ {suites, "tests", graphql_SUITE}. {suites, "tests", graphql_account_SUITE}. {suites, "tests", graphql_domain_SUITE}. +{suites, "tests", graphql_last_SUITE}. {suites, "tests", graphql_muc_SUITE}. {suites, "tests", graphql_muc_light_SUITE}. {suites, "tests", graphql_private_SUITE}. From 8eed2131d1c3b2cbddec089ee4f51d78a460a657 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Wojtasik?= Date: Thu, 19 May 2022 09:51:20 +0200 Subject: [PATCH 10/16] Move old users management to mod_last_api --- src/mod_last_api.erl | 73 ++++++++++++++++++++++- src/mongoose_account_api.erl | 110 +---------------------------------- 2 files changed, 73 insertions(+), 110 deletions(-) diff --git a/src/mod_last_api.erl b/src/mod_last_api.erl index 994a64e126..cb87ad9b5a 100644 --- a/src/mod_last_api.erl +++ b/src/mod_last_api.erl @@ -3,14 +3,20 @@ -export([get_last/1, set_last/3, count_active_users/2]). +-export([list_old_users/1, list_old_users/2, + remove_old_users/1, remove_old_users/2]). + -include("jlib.hrl"). +-include("mongoose.hrl"). +-type old_user() :: {jid:jid(), null | timestamp()}. -type info() :: #{timestamp := mod_last:timestamp(), status := mod_last:status()}. -type timestamp() :: mod_last:timestamp(). -type status() :: mod_last:status(). -define(USER_NOT_FOUND_RESULT(User, Server), {user_does_not_exist, io_lib:format("User ~s@~s does not exist", [User, Server])}). +-define(DOMAIN_NOT_FOUND_RESULT, {domain_not_found, "Domain not found"}). -spec set_last(jid:jid(), timestamp(), status()) -> {ok, info()} | {user_does_not_exist, iolist()}. set_last(#jid{luser = User, lserver = Server} = JID, Timestamp, Status) -> @@ -40,12 +46,75 @@ get_last(#jid{luser = User, lserver = Server} = JID) -> end. -spec count_active_users(jid:server(), timestamp()) -> - {ok, pos_integer()} |{domain_not_found, iolist()}. + {ok, non_neg_integer()} |{domain_not_found, iolist()}. count_active_users(Domain, Timestamp) -> LDomain = jid:nameprep(Domain), case mongoose_domain_api:get_host_type(LDomain) of {ok, HostType} -> {ok, mod_last:count_active_users(HostType, LDomain, Timestamp)}; {error, not_found} -> - {domain_not_found, "Domain not found"} + ?DOMAIN_NOT_FOUND_RESULT + end. + +-spec list_old_users(timestamp()) -> [old_user()]. +list_old_users(Timestamp) -> + lists:append([list_old_users(HostType, Domain, Timestamp) || + HostType <- ?ALL_HOST_TYPES, + Domain <- mongoose_domain_api:get_domains_by_host_type(HostType)]). + +-spec list_old_users(jid:server(), timestamp()) -> + {ok, [old_user()]} | {domain_not_found, iolist()}. +list_old_users(Domain, Timestamp) -> + LDomain = jid:nameprep(Domain), + case mongoose_domain_api:get_host_type(LDomain) of + {ok, HostType} -> + {ok, list_old_users(HostType, LDomain, Timestamp)}; + {error, not_found} -> + ?DOMAIN_NOT_FOUND_RESULT + end. + +-spec remove_old_users(timestamp()) -> [old_user()]. +remove_old_users(Timestamp) -> + OldUsers = list_old_users(Timestamp), + ok = remove_users(OldUsers), + OldUsers. + +-spec remove_old_users(jid:server(), timestamp()) -> + {ok, [old_user()]} | {domain_not_found, iolist()}. +remove_old_users(Domain, Timestamp) -> + case list_old_users(Domain, Timestamp) of + {ok, OldUsers} -> + ok = remove_users(OldUsers), + {ok, OldUsers}; + Error -> + Error end. + +%% Internal + +-spec list_old_users(_, jid:lserver(), timestamp()) -> [old_user()]. +list_old_users(HostType, Domain, Timestamp) -> + Users = ejabberd_auth:get_vh_registered_users(Domain), + lists:filtermap(fun(U) -> prepare_old_user(HostType, U, Timestamp) end, Users). + +-spec prepare_old_user(mongooseim:host_type(), jid:simple_bare_jid(), timestamp()) -> + false | {true, old_user()}. +prepare_old_user(HostType, {LU, LS}, Timestamp) -> + JID = jid:make_bare(LU, LS), + case ejabberd_sm:get_user_resources(JID) of + [] -> + case mod_last:get_last_info(HostType, LU, LS) of + {ok, UserTimestamp, _} when UserTimestamp < Timestamp -> + {true, {JID, UserTimestamp}}; + not_found -> + {true, {JID, null}}; + _ -> + false + end; + _ -> + false + end. + +-spec remove_users([old_user()]) -> ok. +remove_users(Users) -> + lists:foreach(fun({JID, _}) -> ok = ejabberd_auth:remove_user(JID) end, Users). diff --git a/src/mongoose_account_api.erl b/src/mongoose_account_api.erl index 1383f35c85..80117fd1aa 100644 --- a/src/mongoose_account_api.erl +++ b/src/mongoose_account_api.erl @@ -3,14 +3,10 @@ -export([list_users/1, count_users/1, - list_old_users/1, - list_old_users_for_domain/2, register_user/3, register_generated_user/2, unregister_user/1, unregister_user/2, - delete_old_users/1, - delete_old_users_for_domain/2, ban_account/2, ban_account/3, change_password/2, @@ -20,10 +16,7 @@ check_password/2, check_password/3, check_password_hash/3, - check_password_hash/4, - num_active_users/2]). - --include("mongoose.hrl"). + check_password_hash/4]). -type register_result() :: {ok | exists | invalid_jid | cannot_register, iolist()}. @@ -37,18 +30,12 @@ -type check_account_result() :: {ok | user_does_not_exist, string()}. --type num_active_users_result() :: {ok, non_neg_integer()} | {cannot_count, string()}. - --type delete_old_users_result() :: {ok, iolist()}. - -export_type([register_result/0, unregister_result/0, change_password_result/0, check_password_result/0, check_password_hash_result/0, - check_account_result/0, - num_active_users_result/0, - delete_old_users_result/0]). + check_account_result/0]). %% API @@ -179,20 +166,6 @@ check_password_hash(JID, PasswordHash, HashMethod) -> {incorrect, "Password hash is incorrect"} end. --spec num_active_users(jid:lserver(), integer()) -> num_active_users_result(). -num_active_users(Domain, Days) -> - TimeStamp = erlang:system_time(second), - TS = TimeStamp - Days * 86400, - try - {ok, HostType} = mongoose_domain_api:get_domain_host_type(Domain), - Num = mod_last:count_active_users(HostType, Domain, TS), - {ok, Num} - catch - _:_ -> - Message = io_lib:format("Cannot count active users for domain ~s", [Domain]), - {cannot_count, Message} - end. - -spec ban_account(jid:user(), jid:server(), binary()) -> change_password_result(). ban_account(User, Host, ReasonText) -> JID = jid:make(User, Host, <<>>), @@ -210,87 +183,8 @@ ban_account(JID, ReasonText) -> format_change_password(ErrResult) end. --spec delete_old_users(non_neg_integer()) -> {delete_old_users_result(), [jid:literal_jid()]}. -delete_old_users(Days) -> - Users = list_old_users_raw(Days), - DeletedUsers = delete_users(Users), - {{ok, format_deleted_users(DeletedUsers)}, DeletedUsers}. - --spec delete_old_users_for_domain(binary(), non_neg_integer()) -> - {delete_old_users_result(), [jid:literal_jid()]}. -delete_old_users_for_domain(Domain, Days) -> - Users = list_old_users_raw(Domain, Days), - DeletedUsers = delete_users(Users), - {{ok, format_deleted_users(DeletedUsers)}, DeletedUsers}. - --spec list_old_users(non_neg_integer()) -> {ok, [jid:literal_jid()]}. -list_old_users(Days) -> - Users = list_old_users_raw(Days), - {ok, lists:map(fun jid:to_binary/1, Users)}. - --spec list_old_users_for_domain(binary(), non_neg_integer()) -> {ok, [jid:literal_jid()]}. -list_old_users_for_domain(Domain, Days) -> - Users = list_old_users_raw(Domain, Days), - {ok, lists:map(fun jid:to_binary/1, Users)}. - %% Internal --spec delete_users([jid:simple_bare_jid()]) -> [jid:literal_jid()]. -delete_users(Users) -> - lists:map(fun({LUser, LServer}) -> - JID = jid:make(LUser, LServer, <<>>), - ok = ejabberd_auth:remove_user(JID), - jid:to_binary(JID) - end, Users). - --spec list_old_users_raw(non_neg_integer()) -> [jid:simple_bare_jid()]. -list_old_users_raw(Days) -> - lists:append([list_old_users_raw(Domain, Days) || - HostType <- ?ALL_HOST_TYPES, - Domain <- mongoose_domain_api:get_domains_by_host_type(HostType)]). - --spec list_old_users_raw(jid:lserver(), non_neg_integer()) -> [jid:simple_bare_jid()]. -list_old_users_raw(Domain, Days) -> - Users = ejabberd_auth:get_vh_registered_users(Domain), - % Convert older time - SecOlder = Days * 24 * 60 * 60, - % Get current time - TimeStampNow = erlang:system_time(second), - % Filter old users - lists:filter(fun(User) -> is_old_user(User, TimeStampNow, SecOlder) end, Users). - --spec is_old_user(jid:simple_bare_jid(), non_neg_integer(), non_neg_integer()) -> boolean(). -is_old_user({LUser, LServer}, TimeStampNow, SecOlder) -> - JID = jid:make(LUser, LServer, <<>>), - % Check if the user is logged - case ejabberd_sm:get_user_resources(JID) of - [] -> is_user_nonactive_long_enough(JID, TimeStampNow, SecOlder); - _ -> false - end. - --spec is_user_nonactive_long_enough(jid:jid(), non_neg_integer(), non_neg_integer()) -> boolean(). -is_user_nonactive_long_enough(JID, TimeStampNow, SecOlder) -> - {LUser, LServer} = jid:to_lus(JID), - {ok, HostType} = mongoose_domain_api:get_domain_host_type(LServer), - case mod_last:get_last_info(HostType, LUser, LServer) of - {ok, TimeStamp, _Status} -> - % Get user age - Sec = TimeStampNow - TimeStamp, - case Sec < SecOlder of - % Younger than SecOlder - true -> - false; - %% Older - false -> - true - end; - not_found -> - true - end. - -format_deleted_users(Users) -> - io_lib:format("Deleted ~p users: ~p", [length(Users), Users]). - format_change_password(ok) -> {ok, "Password changed"}; format_change_password({error, empty_password}) -> From 1632027d1f1edc2b2ddabea9a877eaa3cdabcf0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Wojtasik?= Date: Thu, 19 May 2022 09:52:56 +0200 Subject: [PATCH 11/16] Move old users queries to the last gql category --- priv/graphql/schemas/admin/account.gql | 20 -------------------- priv/graphql/schemas/admin/last.gql | 23 +++++++++++++++++++++++ priv/graphql/schemas/global/last.gql | 3 +++ 3 files changed, 26 insertions(+), 20 deletions(-) diff --git a/priv/graphql/schemas/admin/account.gql b/priv/graphql/schemas/admin/account.gql index 0b215e3915..77956b402e 100644 --- a/priv/graphql/schemas/admin/account.gql +++ b/priv/graphql/schemas/admin/account.gql @@ -5,15 +5,9 @@ type AccountAdminQuery @protected{ "List users per domain" listUsers(domain: String!): [String!]! @protected(type: DOMAIN, args: ["domain"]) - "List users that didn't log in the last days or have never logged in. Globally or for a specified domain" - listOldUsers(domain: String, days: Int!): [String!]! - @protected(type: DOMAIN, args: ["domain"]) "Get number of users per domain" countUsers(domain: String!): Int! @protected(type: DOMAIN, args: ["domain"]) - "Get number of users active in last days" - countActiveUsers(domain: String!, days: Int!): Int - @protected(type: DOMAIN, args: ["domain"]) "Check if a password is correct" checkPassword(user: JID!, password: String!): CheckPasswordPayload @protected(type: DOMAIN, args: ["user"]) @@ -35,12 +29,6 @@ type AccountAdminMutation @protected{ "Remove a user" removeUser(user: JID!): UserPayload @protected(type: DOMAIN, args: ["user"]) - """ - Delete users that didn't log in the last days or have never logged in. Globally or for a specified domain. - Please use listOldUsers to check which users will be deleted - """ - removeOldUsers(domain: String, days: Int!): OldUsersPayload - @protected(type: DOMAIN, args: ["domain"]) "Ban an account: kick sessions and set a random password" banUser(user: JID!, reason: String!): UserPayload @protected(type: DOMAIN, args: ["user"]) @@ -49,14 +37,6 @@ type AccountAdminMutation @protected{ @protected(type: DOMAIN, args: ["user"]) } -"Remove old users payload" -type OldUsersPayload{ - "Removed users" - users: [JID!]! - "Result message" - message: String! -} - "Modify user payload" type UserPayload{ "User JID" diff --git a/priv/graphql/schemas/admin/last.gql b/priv/graphql/schemas/admin/last.gql index 7b8b432e67..05d182fde1 100644 --- a/priv/graphql/schemas/admin/last.gql +++ b/priv/graphql/schemas/admin/last.gql @@ -8,6 +8,12 @@ type LastAdminQuery @protected{ "Get the number of users active from the given timestamp" countActiveUsers(domain: String!, timestamp: DateTime): Int @protected(type: DOMAIN, args: ["domain"]) + """ + List users that didn't log in the last days or have never logged in. + Globally or for a specified domain + """ + listOldUsers(domain: String, timestamp: DateTime!): [OldUser!] + @protected(type: DOMAIN, args: ["domain"]) } """ @@ -17,4 +23,21 @@ type LastAdminMutation @protected{ "Set user's last activity information" setLast(user: JID!, timestamp: DateTime, status: String!): LastActivity @protected(type: DOMAIN, args: ["user"]) + """ + Delete users that didn't log in the last days or have never logged in. + Globally or for a specified domain. Please use listOldUsers to check which users will be deleted + """ + removeOldUsers(domain: String, timestamp: DateTime!): [OldUser!] + @protected(type: DOMAIN, args: ["domain"]) +} + +""" +The user is considered old when never registered activity in `mod_last`, or the latest registered +activity is older than the given timestamp. +""" +type OldUser{ + "The user's JID" + jid: JID! + "The latest recorded user activity DateTime" + timestamp: DateTime } diff --git a/priv/graphql/schemas/global/last.gql b/priv/graphql/schemas/global/last.gql index 5774e222ef..8f52d0e79a 100644 --- a/priv/graphql/schemas/global/last.gql +++ b/priv/graphql/schemas/global/last.gql @@ -1,5 +1,8 @@ type LastActivity{ + "The user's JID" user: JID! + "The activity DateTime" timestamp: DateTime! + "The status text" status: String! } From dcb084149583a928eaa4b0090e7f3cdcc10bd883 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Wojtasik?= Date: Thu, 19 May 2022 09:57:28 +0200 Subject: [PATCH 12/16] Move handling of the old users to the last admin resolver Remove old users from the account admin resolver --- ...ongoose_graphql_account_admin_mutation.erl | 15 -------------- .../mongoose_graphql_account_admin_query.erl | 19 ------------------ .../mongoose_graphql_last_admin_mutation.erl | 20 ++++++++++++++++++- .../mongoose_graphql_last_admin_query.erl | 20 ++++++++++++++++++- src/graphql/mongoose_graphql_last_helper.erl | 15 ++++++++++++-- 5 files changed, 51 insertions(+), 38 deletions(-) diff --git a/src/graphql/admin/mongoose_graphql_account_admin_mutation.erl b/src/graphql/admin/mongoose_graphql_account_admin_mutation.erl index 0af9fed529..7d5d266539 100644 --- a/src/graphql/admin/mongoose_graphql_account_admin_mutation.erl +++ b/src/graphql/admin/mongoose_graphql_account_admin_mutation.erl @@ -13,8 +13,6 @@ execute(_Ctx, _Obj, <<"registerUser">>, Args) -> register_user(Args); execute(_Ctx, _Obj, <<"removeUser">>, Args) -> remove_user(Args); -execute(_Ctx, _Obj, <<"removeOldUsers">>, Args) -> - remove_old_users(Args); execute(_Ctx, _Obj, <<"banUser">>, Args) -> ban_user(Args); execute(_Ctx, _Obj, <<"changeUserPassword">>, Args) -> @@ -41,14 +39,6 @@ remove_user(#{<<"user">> := JID}) -> Result = mongoose_account_api:unregister_user(JID), format_user_payload(Result, JID). --spec remove_old_users(map()) -> {ok, map()} | {error, resolver_error()}. -remove_old_users(#{<<"domain">> := null, <<"days">> := Days}) -> - {{ok, Msg}, JIDs} = mongoose_account_api:delete_old_users(Days), - {ok, make_old_users_payload(Msg, JIDs)}; -remove_old_users(#{<<"domain">> := Domain, <<"days">> := Days}) -> - {{ok, Msg}, JIDs} = mongoose_account_api:delete_old_users_for_domain(Domain, Days), - {ok, make_old_users_payload(Msg, JIDs)}. - -spec ban_user(map()) -> {ok, map()} | {error, resolver_error()}. ban_user(#{<<"user">> := JID, <<"reason">> := Reason}) -> Result = mongoose_account_api:ban_account(JID, Reason), @@ -71,8 +61,3 @@ format_user_payload(InResult, JID) -> -spec make_user_payload(string(), jid:literal_jid()) -> map(). make_user_payload(Msg, JID) -> #{<<"message">> => iolist_to_binary(Msg), <<"jid">> => JID}. - --spec make_old_users_payload(string(), [jid:literal_jid()]) -> map(). -make_old_users_payload(Msg, JIDs) -> - Users = lists:map(fun(U) -> {ok, U} end, JIDs), - #{<<"message">> => iolist_to_binary(Msg), <<"users">> => Users}. diff --git a/src/graphql/admin/mongoose_graphql_account_admin_query.erl b/src/graphql/admin/mongoose_graphql_account_admin_query.erl index 5f886d07a7..1b75ab5198 100644 --- a/src/graphql/admin/mongoose_graphql_account_admin_query.erl +++ b/src/graphql/admin/mongoose_graphql_account_admin_query.erl @@ -11,12 +11,8 @@ execute(_Ctx, _Obj, <<"listUsers">>, Args) -> list_users(Args); -execute(_Ctx, _Obj, <<"listOldUsers">>, Args) -> - list_old_users(Args); execute(_Ctx, _Obj, <<"countUsers">>, Args) -> count_users(Args); -execute(_Ctx, _Obj, <<"countActiveUsers">>, Args) -> - get_active_users_number(Args); execute(_Ctx, _Obj, <<"checkPassword">>, Args) -> check_password(Args); execute(_Ctx, _Obj, <<"checkPasswordHash">>, Args) -> @@ -32,25 +28,10 @@ list_users(#{<<"domain">> := Domain}) -> Users2 = lists:map(fun(U) -> {ok, U} end, Users), {ok, Users2}. --spec list_old_users(map()) -> {ok, [{ok, binary()}]}. -list_old_users(#{<<"domain">> := null, <<"days">> := Days}) -> - {ok, Users} = mongoose_account_api:list_old_users(Days), - Users2 = lists:map(fun(U) -> {ok, U} end, Users), - {ok, Users2}; -list_old_users(#{<<"domain">> := Domain, <<"days">> := Days}) -> - {ok, Users} = mongoose_account_api:list_old_users_for_domain(Domain, Days), - Users2 = lists:map(fun(U) -> {ok, U} end, Users), - {ok, Users2}. - -spec count_users(map()) -> {ok, non_neg_integer()}. count_users(#{<<"domain">> := Domain}) -> {ok, mongoose_account_api:count_users(Domain)}. --spec get_active_users_number(map()) -> {ok, non_neg_integer()} | {error, resolver_error()}. -get_active_users_number(#{<<"domain">> := Domain, <<"days">> := Days}) -> - Result = mongoose_account_api:num_active_users(Domain, Days), - format_result(Result, #{domain => Domain}). - -spec check_password(map()) -> {ok, map()} | {error, resolver_error()}. check_password(#{<<"user">> := JID, <<"password">> := Password}) -> case mongoose_account_api:check_password(JID, Password) of diff --git a/src/graphql/admin/mongoose_graphql_last_admin_mutation.erl b/src/graphql/admin/mongoose_graphql_last_admin_mutation.erl index 37e4767192..55a1a6f2ae 100644 --- a/src/graphql/admin/mongoose_graphql_last_admin_mutation.erl +++ b/src/graphql/admin/mongoose_graphql_last_admin_mutation.erl @@ -8,13 +8,31 @@ -include("../mongoose_graphql_types.hrl"). -import(mongoose_graphql_helper, [make_error/2]). +-import(mongoose_graphql_last_helper, [format_old_users/1]). +-type old_users() :: mongoose_graphql_last_helper:old_users(). -type last_info() :: mongoose_graphql_last_helper:last_info(). -type args() :: mongoose_graphql:args(). execute(_Ctx, last, <<"setLast">>, Args) -> - set_last(Args). + set_last(Args); +execute(_Ctx, last, <<"removeOldUsers">>, Args) -> + remove_old_users(Args). -spec set_last(args()) -> {ok, last_info()} | {error, resolver_error()}. set_last(#{<<"user">> := JID, <<"timestamp">> := Timestamp, <<"status">> := Status}) -> mongoose_graphql_last_helper:set_last(JID, Timestamp, Status). + +-spec remove_old_users(args()) -> {ok, old_users()} | {error, resolver_error()}. +remove_old_users(#{<<"domain">> := null, <<"timestamp">> := Timestamp}) -> + Timestamp2 = mongoose_graphql_last_helper:microseconds_to_seconds(Timestamp), + OldUsers = mod_last_api:remove_old_users(Timestamp2), + {ok, format_old_users(OldUsers)}; +remove_old_users(#{<<"domain">> := Domain, <<"timestamp">> := Timestamp}) -> + Timestamp2 = mongoose_graphql_last_helper:microseconds_to_seconds(Timestamp), + case mod_last_api:remove_old_users(Domain, Timestamp2) of + {ok, OldUsers} -> + {ok, format_old_users(OldUsers)}; + Error -> + make_error(Error, #{domain => Domain}) + end. diff --git a/src/graphql/admin/mongoose_graphql_last_admin_query.erl b/src/graphql/admin/mongoose_graphql_last_admin_query.erl index 3362179f6a..d15be950ac 100644 --- a/src/graphql/admin/mongoose_graphql_last_admin_query.erl +++ b/src/graphql/admin/mongoose_graphql_last_admin_query.erl @@ -8,14 +8,18 @@ -include("../mongoose_graphql_types.hrl"). -import(mongoose_graphql_helper, [make_error/2, format_result/2, null_to_default/2]). +-import(mongoose_graphql_last_helper, [format_old_users/1]). +-type old_users() :: mongoose_graphql_last_helper:old_users(). -type last_info() :: mongoose_graphql_last_helper:last_info(). -type args() :: mongoose_graphql:args(). execute(_Ctx, last, <<"getLast">>, Args) -> get_last(Args); execute(_Ctx, last, <<"countActiveUsers">>, Args) -> - count_active_users(Args). + count_active_users(Args); +execute(_Ctx, last, <<"listOldUsers">>, Args) -> + list_old_users(Args). -spec get_last(args()) -> {ok, last_info()} | {error, resolver_error()}. get_last(#{<<"user">> := JID}) -> @@ -27,3 +31,17 @@ count_active_users(#{<<"domain">> := Domain, <<"timestamp">> := Timestamp}) -> TimestampSec = mongoose_graphql_last_helper:microseconds_to_seconds(DefTimestamp), Res = mod_last_api:count_active_users(Domain, TimestampSec), format_result(Res, #{domain => Domain}). + +-spec list_old_users(args()) -> {ok, old_users()} | {error, resolver_error()}. +list_old_users(#{<<"domain">> := null, <<"timestamp">> := Timestamp}) -> + Timestamp2 = mongoose_graphql_last_helper:microseconds_to_seconds(Timestamp), + OldUsers = mod_last_api:list_old_users(Timestamp2), + {ok, format_old_users(OldUsers)}; +list_old_users(#{<<"domain">> := Domain, <<"timestamp">> := Timestamp}) -> + Timestamp2 = mongoose_graphql_last_helper:microseconds_to_seconds(Timestamp), + case mod_last_api:list_old_users(Domain, Timestamp2) of + {ok, OldUsers} -> + {ok, format_old_users(OldUsers)}; + Error -> + make_error(Error, #{domain => Domain}) + end. diff --git a/src/graphql/mongoose_graphql_last_helper.erl b/src/graphql/mongoose_graphql_last_helper.erl index 6cd4209dd7..ff060063dc 100644 --- a/src/graphql/mongoose_graphql_last_helper.erl +++ b/src/graphql/mongoose_graphql_last_helper.erl @@ -2,7 +2,7 @@ -export([get_last/1, set_last/3]). --export([microseconds_to_seconds/1, seconds_to_microseconds/1]). +-export([microseconds_to_seconds/1, seconds_to_microseconds/1, format_old_users/1]). -ignore_xref([microseconds_to_seconds/1, seconds_to_microseconds/1]). @@ -10,11 +10,12 @@ -import(mongoose_graphql_helper, [make_error/2, null_to_default/2]). +-type old_users() :: [{ok, map()}]. -type last_info() :: map(). -type timestamp() :: mod_last:timestamp() | null. -type status() :: mod_last:status(). --export_type([last_info/0]). +-export_type([last_info/0, old_users/0]). -spec get_last(jid:jid()) -> {ok, last_info()} | {error, resolver_error()}. get_last(JID) -> @@ -37,8 +38,18 @@ set_last(JID, Timestamp, Status) -> make_error(Error, #{user => jid:to_binary(JID)}) end. +format_old_users(OldUsers) -> + [{ok, make_old_user(JID, Timestamp)} || {JID, Timestamp} <- OldUsers]. + microseconds_to_seconds(Timestamp) -> Timestamp div 1000000. seconds_to_microseconds(Timestamp) -> Timestamp * 1000000. + +%% Internal + +make_old_user(JID, null) -> + #{<<"jid">> => JID, <<"timestamp">> => null}; +make_old_user(JID, Timestamp) -> + #{<<"jid">> => JID, <<"timestamp">> => seconds_to_microseconds(Timestamp)}. From 441a11cd0f5ddf6970a61333ea582a23d5c5372d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Wojtasik?= Date: Thu, 19 May 2022 10:03:33 +0200 Subject: [PATCH 13/16] Move tests for old users to the graphql_last_SUITE Remove all tests using mod_last from the graphql_account_SUITE. --- big_tests/tests/graphql_account_SUITE.erl | 136 +--------------- big_tests/tests/graphql_last_SUITE.erl | 179 ++++++++++++++++++++-- 2 files changed, 168 insertions(+), 147 deletions(-) diff --git a/big_tests/tests/graphql_account_SUITE.erl b/big_tests/tests/graphql_account_SUITE.erl index 091e3d8091..d80cbe3f62 100644 --- a/big_tests/tests/graphql_account_SUITE.erl +++ b/big_tests/tests/graphql_account_SUITE.erl @@ -29,7 +29,6 @@ user_account_handler() -> admin_account_handler() -> [admin_list_users, admin_count_users, - admin_get_active_users_number, admin_check_password, admin_check_password_hash, admin_check_plain_password_hash, @@ -39,11 +38,7 @@ admin_account_handler() -> admin_remove_non_existing_user, admin_remove_existing_user, admin_ban_user, - admin_change_user_password, - admin_list_old_users_domain, - admin_list_old_users_all, - admin_remove_old_users_domain, - admin_remove_old_users_all]. + admin_change_user_password]. init_per_suite(Config) -> Config1 = [{ctl_auth_mods, mongoose_helper:auth_modules()} | Config], @@ -55,9 +50,6 @@ end_per_suite(Config) -> escalus:end_per_suite(Config). init_per_group(admin_account_handler, Config) -> - Mods = [{mod_last, config_parser_helper:default_mod_config(mod_last)}], - dynamic_modules:ensure_modules(domain_helper:host_type(), Mods), - dynamic_modules:ensure_modules(domain_helper:secondary_host_type(), Mods), Config1 = escalus:create_users(Config, escalus:get_users([alice])), graphql_helper:init_admin_handler(Config1); init_per_group(user_account_handler, Config) -> @@ -67,24 +59,12 @@ init_per_group(_, Config) -> end_per_group(admin_account_handler, Config) -> escalus_fresh:clean(), - escalus:delete_users(Config, escalus:get_users([alice])), - dynamic_modules:restore_modules(Config); + escalus:delete_users(Config, escalus:get_users([alice])); end_per_group(user_account_handler, _Config) -> escalus_fresh:clean(); end_per_group(_, _Config) -> ok. -init_per_testcase(C, Config) when C =:= admin_list_old_users_all; - C =:= admin_list_old_users_domain; - C =:= admin_remove_old_users_all; - C =:= admin_remove_old_users_domain -> - {_, AuthMods} = lists:keyfind(ctl_auth_mods, 1, Config), - case lists:member(ejabberd_auth_ldap, AuthMods) of - true -> {skip, not_fully_supported_with_ldap}; - false -> - Config1 = escalus:create_users(Config, escalus:get_users([alice, bob, alice_bis])), - escalus:init_per_testcase(C, Config1) - end; init_per_testcase(admin_register_user = C, Config) -> Config1 = [{user, {<<"gql_admin_registration_test">>, domain_helper:domain()}} | Config], escalus:init_per_testcase(C, Config1); @@ -103,11 +83,6 @@ init_per_testcase(admin_check_plain_password_hash = C, Config) -> init_per_testcase(CaseName, Config) -> escalus:init_per_testcase(CaseName, Config). -end_per_testcase(C, Config) when C =:= admin_list_old_users_all; - C =:= admin_list_old_users_domain; - C =:= admin_remove_old_users_all; - C =:= admin_remove_old_users_domain -> - escalus:delete_users(Config, escalus:get_users([alice, bob, alice_bis])); end_per_testcase(admin_register_user = C, Config) -> {Username, Domain} = proplists:get_value(user, Config), rpc(mim(), mongoose_account_api, unregister_user, [Username, Domain]), @@ -172,14 +147,6 @@ admin_count_users(Config) -> Resp2 = execute_auth(count_users_body(Domain), Config), ?assert(0 < get_ok_value([data, account, countUsers], Resp2)). -admin_get_active_users_number(Config) -> - % Check non-existing domain - Resp = execute_auth(get_active_users_number_body(<<"unknown-domain">>, 5), Config), - ?assertNotEqual(nomatch, binary:match(get_err_msg(Resp), <<"Cannot count">>)), - % Check an existing domain without active users - Resp2 = execute_auth(get_active_users_number_body(domain_helper:domain(), 5), Config), - ?assertEqual(0, get_ok_value([data, account, countActiveUsers], Resp2)). - admin_check_password(Config) -> Password = lists:last(escalus_users:get_usp(Config, alice)), BinJID = escalus_users:get_jid(Config, alice), @@ -297,90 +264,12 @@ admin_change_user_password(Config) -> ?assertNotEqual(nomatch, binary:match(get_ok_value(Path, Resp3), <<"Password changed">>)) end). -admin_remove_old_users_domain(Config) -> - [AliceName, Domain, _] = escalus_users:get_usp(Config, alice), - [BobName, Domain, _] = escalus_users:get_usp(Config, bob), - [AliceBisName, BisDomain, _] = escalus_users:get_usp(Config, alice_bis), - - Now = erlang:system_time(seconds), - set_last(AliceName, Domain, Now), - set_last(BobName, Domain, Now - 86400 * 30), - - Path = [data, account, removeOldUsers, users], - Resp = execute_auth(remove_old_users_body(Domain, 10), Config), - ?assertEqual(1, length(get_ok_value(Path, Resp))), - ?assertMatch({user_does_not_exist, _}, check_account(BobName, Domain)), - ?assertMatch({ok, _}, check_account(AliceName, Domain)), - ?assertMatch({ok, _}, check_account(AliceBisName, BisDomain)). - -admin_remove_old_users_all(Config) -> - [AliceName, Domain, _] = escalus_users:get_usp(Config, alice), - [BobName, Domain, _] = escalus_users:get_usp(Config, bob), - [AliceBisName, BisDomain, _] = escalus_users:get_usp(Config, alice_bis), - - Now = erlang:system_time(seconds), - OldTime = Now - 86400 * 30, - set_last(AliceName, Domain, Now), - set_last(BobName, Domain, OldTime), - set_last(AliceBisName, BisDomain, OldTime), - - Path = [data, account, removeOldUsers, users], - Resp = execute_auth(remove_old_users_body(null, 10), Config), - ?assertEqual(2, length(get_ok_value(Path, Resp))), - ?assertMatch({user_does_not_exist, _}, check_account(BobName, Domain)), - ?assertMatch({ok, _}, check_account(AliceName, Domain)), - ?assertMatch({user_does_not_exist, _}, check_account(AliceBisName, BisDomain)). - -admin_list_old_users_domain(Config) -> - [AliceName, Domain, _] = escalus_users:get_usp(Config, alice), - [BobName, Domain, _] = escalus_users:get_usp(Config, bob), - - Now = erlang:system_time(seconds), - set_last(AliceName, Domain, Now), - set_last(BobName, Domain, Now - 86400 * 30), - - LBob = escalus_utils:jid_to_lower(escalus_users:get_jid(Config, bob)), - - Path = [data, account, listOldUsers], - Resp = execute_auth(list_old_users_body(Domain, 10), Config), - Users = get_ok_value(Path, Resp), - ?assertEqual(1, length(Users)), - ?assert(lists:member(LBob, Users)). - -admin_list_old_users_all(Config) -> - [AliceName, Domain, _] = escalus_users:get_usp(Config, alice), - [BobName, Domain, _] = escalus_users:get_usp(Config, bob), - [AliceBisName, BisDomain, _] = escalus_users:get_usp(Config, alice_bis), - - Now = erlang:system_time(seconds), - OldTime = Now - 86400 * 30, - set_last(AliceName, Domain, Now), - set_last(BobName, Domain, OldTime), - set_last(AliceBisName, BisDomain, OldTime), - - LBob = escalus_utils:jid_to_lower(escalus_users:get_jid(Config, bob)), - LAliceBis = escalus_utils:jid_to_lower(escalus_users:get_jid(Config, alice_bis)), - - Path = [data, account, listOldUsers], - Resp = execute_auth(list_old_users_body(null, 10), Config), - Users = get_ok_value(Path, Resp), - ?assertEqual(2, length(Users)), - ?assert(lists:member(LBob, Users)), - ?assert(lists:member(LAliceBis, Users)). - %% Helpers get_md5(AccountPass) -> lists:flatten([io_lib:format("~.16B", [X]) || X <- binary_to_list(crypto:hash(md5, AccountPass))]). -set_last(User, Domain, TStamp) -> - rpc(mim(), mod_last, store_last_info, - [domain_helper:host_type(), escalus_utils:jid_to_lower(User), Domain, TStamp, <<>>]). - -check_account(Username, Domain) -> - rpc(mim(), mongoose_account_api, check_account, [Username, Domain]). - %% Request bodies list_users_body(Domain) -> @@ -395,13 +284,6 @@ count_users_body(Domain) -> Vars = #{<<"domain">> => Domain}, #{query => Query, operationName => OpName, variables => Vars}. -get_active_users_number_body(Domain, Days) -> - Query = <<"query Q1($domain: String!, $days: Int!) - { account { countActiveUsers(domain: $domain, days: $days) } }">>, - OpName = <<"Q1">>, - Vars = #{<<"domain">> => Domain, <<"days">> => Days}, - #{query => Query, operationName => OpName, variables => Vars}. - check_password_body(User, Password) -> Query = <<"query Q1($user: JID!, $password: String!) { account { checkPassword(user: $user, password: $password) {correct message} } }">>, @@ -439,20 +321,6 @@ remove_user_body(User) -> Vars = #{<<"user">> => User}, #{query => Query, operationName => OpName, variables => Vars}. -remove_old_users_body(Domain, Days) -> - Query = <<"mutation M1($domain: String, $days: Int!) - { account { removeOldUsers(domain: $domain, days: $days) { message users } } }">>, - OpName = <<"M1">>, - Vars = #{<<"domain">> => Domain, <<"days">> => Days}, - #{query => Query, operationName => OpName, variables => Vars}. - -list_old_users_body(Domain, Days) -> - Query = <<"query Q1($domain: String, $days: Int!) - { account { listOldUsers(domain: $domain, days: $days) } }">>, - OpName = <<"Q1">>, - Vars = #{<<"domain">> => Domain, <<"days">> => Days}, - #{query => Query, operationName => OpName, variables => Vars}. - ban_user_body(JID, Reason) -> Query = <<"mutation M1($user: JID!, $reason: String!) { account { banUser(user: $user, reason: $reason) { jid message } } }">>, diff --git a/big_tests/tests/graphql_last_SUITE.erl b/big_tests/tests/graphql_last_SUITE.erl index 042ced51a1..ca044ca460 100644 --- a/big_tests/tests/graphql_last_SUITE.erl +++ b/big_tests/tests/graphql_last_SUITE.erl @@ -3,7 +3,7 @@ -compile([export_all, nowarn_export_all]). -import(distributed_helper, [mim/0, require_rpc_nodes/1, rpc/4]). --import(graphql_helper, [execute_user/3, execute_auth/2, user_to_bin/1, +-import(graphql_helper, [execute_user/3, execute_auth/2, user_to_bin/1, user_to_jid/1, get_ok_value/2, get_err_msg/1, get_err_code/1]). -include_lib("eunit/include/eunit.hrl"). @@ -19,11 +19,13 @@ suite() -> all() -> [{group, user_last}, - {group, admin_last}]. + {group, admin_last}, + {group, admin_last_old_users}]. groups() -> [{user_last, [parallel], user_last_handler()}, - {admin_last, [parallel], admin_last_handler()}]. + {admin_last, [parallel], admin_last_handler()}, + {admin_last_old_users, [], admin_old_users_handler()}]. user_last_handler() -> [user_set_last, @@ -39,14 +41,27 @@ admin_last_handler() -> admin_count_active_users, admin_try_count_nonexistent_domain_active_users]. +admin_old_users_handler() -> + [admin_list_old_users_domain, + admin_try_list_old_users_nonexistent_domain, + admin_list_old_users_global, + admin_remove_old_users_domain, + admin_try_remove_old_users_nonexistent_domain, + admin_remove_old_users_global, + admin_user_without_last_info_is_old_user, + admin_logged_user_is_not_old_user]. + init_per_suite(Config) -> - Config1 = escalus:init_per_suite(Config), - Config2 = dynamic_modules:save_modules(domain_helper:host_type(), Config1), HostType = domain_helper:host_type(), + SecHostType = domain_helper:secondary_host_type(), + Config1 = escalus:init_per_suite(Config), + Config2 = dynamic_modules:save_modules(HostType, Config1), + Config3 = dynamic_modules:save_modules(SecHostType, Config2), Backend = mongoose_helper:get_backend_mnesia_rdbms_riak(HostType), + SecBackend = mongoose_helper:get_backend_mnesia_rdbms_riak(SecHostType), dynamic_modules:ensure_modules(HostType, required_modules(Backend)), - escalus:init_per_suite([{backend, Backend} | Config2]). - + dynamic_modules:ensure_modules(SecHostType, required_modules(SecBackend)), + escalus:init_per_suite(Config3). end_per_suite(Config) -> dynamic_modules:restore_modules(Config), @@ -54,17 +69,35 @@ end_per_suite(Config) -> init_per_group(admin_last, Config) -> graphql_helper:init_admin_handler(Config); +init_per_group(admin_last_old_users, Config) -> + AuthMods = mongoose_helper:auth_modules(), + case lists:member(ejabberd_auth_ldap, AuthMods) of + true -> {skip, not_fully_supported_with_ldap}; + false -> graphql_helper:init_admin_handler(Config) + end; init_per_group(user_last, Config) -> [{schema_endpoint, user} | Config]. -end_per_group(admin_last, _Config) -> - escalus_fresh:clean(); -end_per_group(user_last, _Config) -> +end_per_group(_, _Config) -> escalus_fresh:clean(). +init_per_testcase(C, Config) when C =:= admin_remove_old_users_domain; + C =:= admin_remove_old_users_global; + C =:= admin_list_old_users_domain; + C =:= admin_list_old_users_global; + C =:= admin_user_without_last_info_is_old_user -> + Config1 = escalus:create_users(Config, escalus:get_users([alice, bob, alice_bis])), + escalus:init_per_testcase(C, Config1); init_per_testcase(CaseName, Config) -> escalus:init_per_testcase(CaseName, Config). +end_per_testcase(C, Config) when C =:= admin_remove_old_users_domain; + C =:= admin_remove_old_users_global; + C =:= admin_list_old_users_domain; + C =:= admin_list_old_users_global; + C =:= admin_user_without_last_info_is_old_user -> + escalus:delete_users(Config, escalus:get_users([alice, bob, alice_bis])), + escalus:end_per_testcase(C, Config); end_per_testcase(CaseName, Config) -> escalus:end_per_testcase(CaseName, Config). @@ -89,7 +122,7 @@ admin_set_last(Config, Alice) -> Res = execute_auth(admin_set_last_body(Alice, Status, ?DEFAULT_DT), Config), #{<<"user">> := JID, <<"status">> := Status, <<"timestamp">> := ?DEFAULT_DT} = get_ok_value(p(setLast), Res), - % Without timestamp + % Without timestamp provided Status2 = <<"Second status">>, Res2 = execute_auth(admin_set_last_body(Alice, Status2, null), Config), #{<<"user">> := JID, <<"status">> := Status2, <<"timestamp">> := DateTime2} = @@ -133,8 +166,8 @@ admin_count_active_users(Config) -> admin_count_active_users(Config, Alice, Bob) -> Domain = domain_helper:domain(), - execute_auth(admin_set_last_body(Alice, <<"a">>, now_dt_with_offset(5)), Config), - execute_auth(admin_set_last_body(Bob, <<"b">>, now_dt_with_offset(10)), Config), + set_last(Alice, now_dt_with_offset(5), Config), + set_last(Bob, now_dt_with_offset(10), Config), Res = execute_auth(admin_count_active_users_body(Domain, null), Config), ?assertEqual(2, get_ok_value(p(countActiveUsers), Res)), Res2 = execute_auth(admin_count_active_users_body(Domain, now_dt_with_offset(30)), Config), @@ -145,6 +178,101 @@ admin_try_count_nonexistent_domain_active_users(Config) -> ?assertErrMsg(Res, <<"not found">>), ?assertErrCode(Res, domain_not_found). +%% Admin old users test cases + +admin_remove_old_users_domain(Config) -> + jids_with_config(Config, [alice, alice_bis, bob], fun admin_remove_old_users_domain/4). + +admin_remove_old_users_domain(Config, Alice, AliceBis, Bob) -> + Domain = domain_helper:domain(), + ToRemoveDateTime = now_dt_with_offset(100), + + set_last(Bob, ToRemoveDateTime, Config), + set_last(AliceBis, ToRemoveDateTime, Config), + set_last(Alice, now_dt_with_offset(200), Config), + + Resp = execute_auth(admin_remove_old_users_body(Domain, now_dt_with_offset(150)), Config), + [#{<<"jid">> := Bob, <<"timestamp">> := BobDateTimeRes}] = get_ok_value(p(removeOldUsers), Resp), + ?assertEqual(dt_to_unit(ToRemoveDateTime, second), dt_to_unit(BobDateTimeRes, second)), + ?assertMatch({user_does_not_exist, _}, check_account(Bob)), + ?assertMatch({ok, _}, check_account(Alice)), + ?assertMatch({ok, _}, check_account(AliceBis)). + +admin_try_remove_old_users_nonexistent_domain(Config) -> + Res = execute_auth(admin_remove_old_users_body(<<"nonexistent">>, now_dt_with_offset(0)), Config), + ?assertErrMsg(Res, <<"not found">>), + ?assertErrCode(Res, domain_not_found). + +admin_remove_old_users_global(Config) -> + jids_with_config(Config, [alice, alice_bis, bob], fun admin_remove_old_users_global/4). + +admin_remove_old_users_global(Config, Alice, AliceBis, Bob) -> + ToRemoveDateTime = now_dt_with_offset(100), + ToRemoveTimestamp = dt_to_unit(ToRemoveDateTime, second), + + set_last(Bob, ToRemoveDateTime, Config), + set_last(AliceBis, ToRemoveDateTime, Config), + set_last(Alice, now_dt_with_offset(200), Config), + + Resp = execute_auth(admin_remove_old_users_body(null, now_dt_with_offset(150)), Config), + [#{<<"jid">> := AliceBis, <<"timestamp">> := AliceBisDateTime}, + #{<<"jid">> := Bob, <<"timestamp">> := BobDateTime}] = + lists:sort(get_ok_value(p(removeOldUsers), Resp)), + ?assertEqual(ToRemoveTimestamp, dt_to_unit(BobDateTime, second)), + ?assertEqual(ToRemoveTimestamp, dt_to_unit(AliceBisDateTime, second)), + ?assertMatch({user_does_not_exist, _}, check_account(Bob)), + ?assertMatch({user_does_not_exist, _}, check_account(AliceBis)), + ?assertMatch({ok, _}, check_account(Alice)). + +admin_list_old_users_domain(Config) -> + jids_with_config(Config, [alice, bob], fun admin_list_old_users_domain/3). + +admin_list_old_users_domain(Config, Alice, Bob) -> + Domain = domain_helper:domain(), + OldDateTime = now_dt_with_offset(100), + + set_last(Bob, OldDateTime, Config), + set_last(Alice, now_dt_with_offset(200), Config), + + Res = execute_auth(admin_list_old_users_body(Domain, now_dt_with_offset(150)), Config), + [#{<<"jid">> := Bob, <<"timestamp">> := BobDateTime}] = get_ok_value(p(listOldUsers), Res), + ?assertEqual(dt_to_unit(OldDateTime, second), dt_to_unit(BobDateTime, second)). + +admin_try_list_old_users_nonexistent_domain(Config) -> + Res = execute_auth(admin_list_old_users_body(<<"nonexistent">>, now_dt_with_offset(0)), Config), + ?assertErrMsg(Res, <<"not found">>), + ?assertErrCode(Res, domain_not_found). + +admin_list_old_users_global(Config) -> + jids_with_config(Config, [alice, alice_bis, bob], fun admin_list_old_users_global/4). + +admin_list_old_users_global(Config, Alice, AliceBis, Bob) -> + OldDateTime = now_dt_with_offset(100), + + set_last(Bob, OldDateTime, Config), + set_last(AliceBis, OldDateTime, Config), + set_last(Alice, now_dt_with_offset(200), Config), + + Res = execute_auth(admin_list_old_users_body(null, now_dt_with_offset(150)), Config), + [#{<<"jid">> := AliceBis, <<"timestamp">> := AliceBisDateTime}, + #{<<"jid">> := Bob, <<"timestamp">> := BobDateTime}] = + lists:sort(get_ok_value(p(listOldUsers), Res)), + ?assertEqual(dt_to_unit(OldDateTime, second), dt_to_unit(BobDateTime, second)), + ?assertEqual(dt_to_unit(OldDateTime, second), dt_to_unit(AliceBisDateTime, second)). + +admin_user_without_last_info_is_old_user(Config) -> + Res = execute_auth(admin_list_old_users_body(null, now_dt_with_offset(150)), Config), + OldUsers = get_ok_value(p(listOldUsers), Res), + ?assertEqual(3, length(OldUsers)), + [?assertEqual(null, TS) || #{<<"timestamp">> := TS} <- OldUsers]. + +admin_logged_user_is_not_old_user(Config) -> + escalus:fresh_story_with_config(Config, [{alice, 1}], fun admin_logged_user_is_not_old_user/2). + +admin_logged_user_is_not_old_user(Config, _Alice) -> + Res = execute_auth(admin_list_old_users_body(null, now_dt_with_offset(100)), Config), + ?assertEqual([], get_ok_value(p(listOldUsers), Res)). + %% User test cases user_set_last(Config) -> @@ -188,6 +316,17 @@ user_get_other_user_last(Config, Alice, Bob) -> %% Helpers +jids_with_config(Config, Users, Fun) -> + Args = [escalus_utils:jid_to_lower(escalus_users:get_jid(Config, User)) || User <- Users], + apply(Fun, [Config | Args]). + +set_last(UserJID, DateTime, Config) -> + execute_auth(admin_set_last_body(UserJID, <<>>, DateTime), Config). + +check_account(User) -> + {Username, LServer} = jid:to_lus(user_to_jid(User)), + rpc(mim(), mongoose_account_api, check_account, [Username, LServer]). + assert_err_msg(Contains, Res) -> ?assertNotEqual(nomatch, binary:match(get_err_msg(Res), Contains)). @@ -231,6 +370,20 @@ admin_count_active_users_body(Domain, Timestamp) -> Vars = #{domain => Domain, timestamp => Timestamp}, #{query => Query, operationName => OpName, variables => Vars}. +admin_remove_old_users_body(Domain, Timestamp) -> + Query = <<"mutation M1($domain: String, $timestamp: DateTime!) + { last { removeOldUsers(domain: $domain, timestamp: $timestamp) { jid timestamp } } }">>, + OpName = <<"M1">>, + Vars = #{domain => Domain, timestamp => Timestamp}, + #{query => Query, operationName => OpName, variables => Vars}. + +admin_list_old_users_body(Domain, Timestamp) -> + Query = <<"query Q1($domain: String, $timestamp: DateTime!) + { last { listOldUsers(domain: $domain, timestamp: $timestamp) { jid timestamp } } }">>, + OpName = <<"Q1">>, + Vars = #{domain => Domain, timestamp => Timestamp}, + #{query => Query, operationName => OpName, variables => Vars}. + user_set_last_body(Status, DateTime) -> Query = <<"mutation M1($timestamp: DateTime, $status: String!) { last { setLast (timestamp: $timestamp, status: $status) From 589d0927c6721828e9435e2d555018bd83eace8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Wojtasik?= Date: Thu, 19 May 2022 09:55:39 +0200 Subject: [PATCH 14/16] Use mod_last_api in the service_admin_extra_accounts module --- .../service_admin_extra_accounts.erl | 33 ++++++++++++++----- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/src/admin_extra/service_admin_extra_accounts.erl b/src/admin_extra/service_admin_extra_accounts.erl index 5a04c33614..e50c1c7ed2 100644 --- a/src/admin_extra/service_admin_extra_accounts.erl +++ b/src/admin_extra/service_admin_extra_accounts.erl @@ -122,25 +122,40 @@ check_account(User, Host) -> check_password_hash(User, Host, PasswordHash, HashMethod) -> mongoose_account_api:check_password_hash(User, Host, PasswordHash, HashMethod). --spec num_active_users(jid:lserver(), integer()) -> {ok | cannot_count, string()}. +-spec num_active_users(jid:lserver(), integer()) -> {ok | domain_not_found, iolist()}. num_active_users(Domain, Days) -> - case mongoose_account_api:num_active_users(Domain, Days) of + Timestamp2 = days_to_timestamp(Days), + case mod_last_api:count_active_users(Domain, Timestamp2) of {ok, Num} -> {ok, integer_to_list(Num)}; Res -> Res end. --spec delete_old_users(integer()) -> mongoose_account_api:delete_old_users_result(). +-spec delete_old_users(integer()) -> {ok, iolist()}. delete_old_users(Days) -> - {Res, _} = mongoose_account_api:delete_old_users(Days), - Res. + Timestamp = days_to_timestamp(Days), + OldUsers = mod_last_api:remove_old_users(Timestamp), + {ok, format_deleted_users(OldUsers)}. --spec delete_old_users_for_domain(jid:server(), integer()) -> - mongoose_account_api:delete_old_users(). +-spec delete_old_users_for_domain(jid:server(), integer()) -> {ok | domain_not_found, iolist()}. delete_old_users_for_domain(Domain, Days) -> - {Res, _} = mongoose_account_api:delete_old_users_for_domain(Domain, Days), - Res. + Timestamp = days_to_timestamp(Days), + case mod_last_api:remove_old_users(Domain, Timestamp) of + {ok, OldUsers} -> + {ok, format_deleted_users(OldUsers)}; + Error -> + Error + end. -spec ban_account(jid:user(), jid:server(), binary() | string()) -> mongoose_account_api:change_password_result(). ban_account(User, Host, ReasonText) -> mongoose_account_api:ban_account(User, Host, ReasonText). + +%% Internal + +days_to_timestamp(Days) -> + erlang:system_time(second) - Days * 86400. + +format_deleted_users(Users) -> + Users2 = [jid:to_binary(JID) || {JID, _} <- Users], + io_lib:format("Deleted ~p users: ~p", [length(Users), Users2]). From bc48a832e200a5f3c468647bd5c583fedbce52f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Wojtasik?= Date: Thu, 19 May 2022 13:49:00 +0200 Subject: [PATCH 15/16] Permissions - domain admin cannot execute queries with null domain arguments Domain admin shouldn't be able to execute a query containing fields with optional domain arguments without providing them. Null domain argument means that the command should affect the global scope. Thus only global admin should be allowed to do it. --- src/graphql/mongoose_graphql_permissions.erl | 4 ---- test/mongoose_graphql_SUITE.erl | 4 ++-- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/graphql/mongoose_graphql_permissions.erl b/src/graphql/mongoose_graphql_permissions.erl index ff3fe6e7b5..aef3a187d0 100644 --- a/src/graphql/mongoose_graphql_permissions.erl +++ b/src/graphql/mongoose_graphql_permissions.erl @@ -181,10 +181,6 @@ arg_eq(Subdomain, Domain) when is_binary(Subdomain), is_binary(Domain) -> check_subdomain(Subdomain, Domain); arg_eq(#jid{lserver = Domain1}, Domain2) -> arg_eq(Domain1, Domain2); -arg_eq(undefined, _) -> - % The arg is optional, and the value is not present, so we assume that - % the domain admin has access. - true; arg_eq(_, _) -> false. diff --git a/test/mongoose_graphql_SUITE.erl b/test/mongoose_graphql_SUITE.erl index a9cd6848a7..ede1a58dc9 100644 --- a/test/mongoose_graphql_SUITE.erl +++ b/test/mongoose_graphql_SUITE.erl @@ -542,8 +542,8 @@ check_field_list_arg_domain_permissions(Config) -> check_field_null_arg_domain_permissions(Config) -> [{_, Domain} | _] = ?config(domains, Config), - Doc = <<"{ domainProtectedField domainInputProtectedField }">>, - ?assertPermissionsSuccess(Config, Domain, Doc). + FDoc = <<"{ domainProtectedField domainInputProtectedField }">>, + ?assertDomainPermissionsFailed(Config, Domain, [<<"argA">>], FDoc). check_field_jid_arg_domain_permissions(Config) -> Domain = <<"my-domain.com">>, From 61a918c089bf6ac885807d3f214400cecc7b7bcf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Wojtasik?= Date: Fri, 20 May 2022 12:38:06 +0200 Subject: [PATCH 16/16] Apply review comments --- big_tests/tests/graphql_last_SUITE.erl | 5 ++--- .../service_admin_extra_accounts.erl | 4 ++-- src/mod_last_api.erl | 17 +++++++++-------- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/big_tests/tests/graphql_last_SUITE.erl b/big_tests/tests/graphql_last_SUITE.erl index ca044ca460..93d9be528e 100644 --- a/big_tests/tests/graphql_last_SUITE.erl +++ b/big_tests/tests/graphql_last_SUITE.erl @@ -55,13 +55,12 @@ init_per_suite(Config) -> HostType = domain_helper:host_type(), SecHostType = domain_helper:secondary_host_type(), Config1 = escalus:init_per_suite(Config), - Config2 = dynamic_modules:save_modules(HostType, Config1), - Config3 = dynamic_modules:save_modules(SecHostType, Config2), + Config2 = dynamic_modules:save_modules([HostType, SecHostType], Config1), Backend = mongoose_helper:get_backend_mnesia_rdbms_riak(HostType), SecBackend = mongoose_helper:get_backend_mnesia_rdbms_riak(SecHostType), dynamic_modules:ensure_modules(HostType, required_modules(Backend)), dynamic_modules:ensure_modules(SecHostType, required_modules(SecBackend)), - escalus:init_per_suite(Config3). + escalus:init_per_suite(Config2). end_per_suite(Config) -> dynamic_modules:restore_modules(Config), diff --git a/src/admin_extra/service_admin_extra_accounts.erl b/src/admin_extra/service_admin_extra_accounts.erl index e50c1c7ed2..3ffea82374 100644 --- a/src/admin_extra/service_admin_extra_accounts.erl +++ b/src/admin_extra/service_admin_extra_accounts.erl @@ -157,5 +157,5 @@ days_to_timestamp(Days) -> erlang:system_time(second) - Days * 86400. format_deleted_users(Users) -> - Users2 = [jid:to_binary(JID) || {JID, _} <- Users], - io_lib:format("Deleted ~p users: ~p", [length(Users), Users2]). + Users2 = lists:join(", ", [jid:to_binary(JID) || {JID, _} <- Users]), + io_lib:format("Deleted ~p users: ~s", [length(Users), Users2]). diff --git a/src/mod_last_api.erl b/src/mod_last_api.erl index cb87ad9b5a..6c15f2c59f 100644 --- a/src/mod_last_api.erl +++ b/src/mod_last_api.erl @@ -13,10 +13,11 @@ -type info() :: #{timestamp := mod_last:timestamp(), status := mod_last:status()}. -type timestamp() :: mod_last:timestamp(). -type status() :: mod_last:status(). +-type host_type() :: mongooseim:host_type(). -define(USER_NOT_FOUND_RESULT(User, Server), {user_does_not_exist, io_lib:format("User ~s@~s does not exist", [User, Server])}). --define(DOMAIN_NOT_FOUND_RESULT, {domain_not_found, "Domain not found"}). +-define(DOMAIN_NOT_FOUND_RESULT, {domain_not_found, <<"Domain not found">>}). -spec set_last(jid:jid(), timestamp(), status()) -> {ok, info()} | {user_does_not_exist, iolist()}. set_last(#jid{luser = User, lserver = Server} = JID, Timestamp, Status) -> @@ -30,7 +31,7 @@ set_last(#jid{luser = User, lserver = Server} = JID, Timestamp, Status) -> end. -spec get_last(jid:jid()) -> - {ok, info()} | {last_not_found | user_does_not_exist, iolist()}. + {ok, info()} | {last_not_found | user_does_not_exist, iodata()}. get_last(#jid{luser = User, lserver = Server} = JID) -> case ejabberd_auth:does_user_exist(JID) of true -> @@ -39,14 +40,14 @@ get_last(#jid{luser = User, lserver = Server} = JID) -> {ok, Timestamp, Status} -> {ok, #{timestamp => Timestamp, status => Status}}; not_found -> - {last_not_found, "Given user's last info not found"} + {last_not_found, <<"Given user's last info not found">>} end; false -> ?USER_NOT_FOUND_RESULT(User, Server) end. -spec count_active_users(jid:server(), timestamp()) -> - {ok, non_neg_integer()} |{domain_not_found, iolist()}. + {ok, non_neg_integer()} | {domain_not_found, binary()}. count_active_users(Domain, Timestamp) -> LDomain = jid:nameprep(Domain), case mongoose_domain_api:get_host_type(LDomain) of @@ -63,7 +64,7 @@ list_old_users(Timestamp) -> Domain <- mongoose_domain_api:get_domains_by_host_type(HostType)]). -spec list_old_users(jid:server(), timestamp()) -> - {ok, [old_user()]} | {domain_not_found, iolist()}. + {ok, [old_user()]} | {domain_not_found, binary()}. list_old_users(Domain, Timestamp) -> LDomain = jid:nameprep(Domain), case mongoose_domain_api:get_host_type(LDomain) of @@ -80,7 +81,7 @@ remove_old_users(Timestamp) -> OldUsers. -spec remove_old_users(jid:server(), timestamp()) -> - {ok, [old_user()]} | {domain_not_found, iolist()}. + {ok, [old_user()]} | {domain_not_found, binary()}. remove_old_users(Domain, Timestamp) -> case list_old_users(Domain, Timestamp) of {ok, OldUsers} -> @@ -92,12 +93,12 @@ remove_old_users(Domain, Timestamp) -> %% Internal --spec list_old_users(_, jid:lserver(), timestamp()) -> [old_user()]. +-spec list_old_users(host_type(), jid:lserver(), timestamp()) -> [old_user()]. list_old_users(HostType, Domain, Timestamp) -> Users = ejabberd_auth:get_vh_registered_users(Domain), lists:filtermap(fun(U) -> prepare_old_user(HostType, U, Timestamp) end, Users). --spec prepare_old_user(mongooseim:host_type(), jid:simple_bare_jid(), timestamp()) -> +-spec prepare_old_user(host_type(), jid:simple_bare_jid(), timestamp()) -> false | {true, old_user()}. prepare_old_user(HostType, {LU, LS}, Timestamp) -> JID = jid:make_bare(LU, LS),