Skip to content

Commit

Permalink
Merge pull request #3633 from esl/graphql/support-admin-per-domain
Browse files Browse the repository at this point in the history
GraphQL - Support admin per domain
  • Loading branch information
chrzaszcz authored Apr 29, 2022
2 parents 46c6bc1 + 2bf7c82 commit a6a323b
Show file tree
Hide file tree
Showing 34 changed files with 696 additions and 73 deletions.
8 changes: 8 additions & 0 deletions big_tests/tests/domain_helper.erl
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
delete_configured_domains/0,
insert_domain/3,
delete_domain/2,
set_domain_password/3,
delete_domain_password/2,
make_metrics_prefix/1,
host_types/0,
host_types/1,
Expand Down Expand Up @@ -66,6 +68,12 @@ insert_persistent_domain(Node, Domain, HostType) ->
delete_persistent_domain(Node, Domain, HostType) ->
ok = rpc(Node, mongoose_domain_api, delete_domain, [Domain, HostType]).

set_domain_password(Node, Domain, Password) ->
ok = rpc(Node, mongoose_domain_api, set_domain_password, [Domain, Password]).

delete_domain_password(Node, Domain) ->
ok = rpc(Node, mongoose_domain_api, delete_domain_password, [Domain]).

for_each_configured_domain(F) ->
[for_each_configured_domain(F, Opts) || {_, Opts} <- ct:get_config(hosts)],
ok.
Expand Down
73 changes: 50 additions & 23 deletions big_tests/tests/graphql_SUITE.erl
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@
-compile([export_all, nowarn_export_all]).

-import(distributed_helper, [mim/0, require_rpc_nodes/1, rpc/4]).
-import(graphql_helper, [execute/3]).
-import(graphql_helper, [execute/3, execute_auth/2, execute_domain_auth/2, execute_user/3]).

-define(assertAdminAuth(Auth, Data), assert_auth(atom_to_binary(Auth), Data)).
-define(assertAdminAuth(Domain, Type, Auth, Data),
assert_auth(#{<<"domain">> => Domain,
<<"authStatus">> => atom_to_binary(Auth),
<<"authType">> => maybe_atom_to_bin(Type)}, Data)).
-define(assertUserAuth(Username, Auth, Data),
assert_auth(#{<<"username">> => Username,
<<"authStatus">> => atom_to_binary(Auth)}, Data)).
Expand All @@ -20,15 +23,18 @@ suite() ->
all() ->
[{group, cowboy_handler},
{group, admin_handler},
{group, domain_admin_handler},
{group, user_handler}].

groups() ->
[{cowboy_handler, [parallel], cowboy_handler()},
{user_handler, [parallel], user_handler()},
{domain_admin_handler, [parallel], domain_admin_handler()},
{admin_handler, [parallel], admin_handler()}].

cowboy_handler() ->
[can_connect_to_admin,
can_connect_to_domain_admin,
can_connect_to_user].

user_handler() ->
Expand All @@ -37,6 +43,9 @@ user_handler() ->
admin_handler() ->
[admin_checks_auth,
auth_admin_checks_auth | common_tests()].
domain_admin_handler() ->
[domain_admin_checks_auth,
auth_domain_admin_checks_auth | common_tests()].

common_tests() ->
[can_load_graphiql].
Expand All @@ -52,14 +61,23 @@ end_per_suite(Config) ->

init_per_group(admin_handler, Config) ->
graphql_helper:init_admin_handler(Config);
init_per_group(domain_admin_handler, Config) ->
case mongoose_helper:is_rdbms_enabled(domain_helper:host_type()) of
true ->
graphql_helper:init_domain_admin_handler(Config);
false ->
{skip, require_rdbms}
end;
init_per_group(user_handler, Config) ->
Config1 = escalus:create_users(Config, escalus:get_users([alice])),
[{schema_endpoint, user} | Config1];
init_per_group(_, Config) ->
init_per_group(cowboy_handler, Config) ->
Config.

end_per_group(user_handler, Config) ->
escalus:delete_users(Config, escalus:get_users([alice]));
end_per_group(domain_admin_handler, Config) ->
graphql_helper:end_domain_admin_handler(Config);
end_per_group(_, _Config) ->
ok.

Expand All @@ -72,6 +90,9 @@ end_per_testcase(CaseName, Config) ->
can_connect_to_admin(_Config) ->
?assertMatch({{<<"400">>, <<"Bad Request">>}, _}, execute(admin, #{}, undefined)).

can_connect_to_domain_admin(_Config) ->
?assertMatch({{<<"400">>, <<"Bad Request">>}, _}, execute(domain_admin, #{}, undefined)).

can_connect_to_user(_Config) ->
?assertMatch({{<<"400">>, <<"Bad Request">>}, _}, execute(user, #{}, undefined)).

Expand All @@ -83,47 +104,44 @@ can_load_graphiql(Config) ->

user_checks_auth(Config) ->
Ep = ?config(schema_endpoint, Config),
Body = #{query => "{ checkAuth { username authStatus } }"},
StatusData = execute(Ep, Body, undefined),
StatusData = execute(Ep, user_check_auth_body(), undefined),
?assertUserAuth(null, 'UNAUTHORIZED', StatusData).

auth_user_checks_auth(Config) ->
escalus:fresh_story(
Config, [{alice, 1}],
fun(Alice) ->
Password = user_password(alice),
AliceJID = escalus_client:short_jid(Alice),
Ep = ?config(schema_endpoint, Config),
Body = #{query => "{ checkAuth { username authStatus } }"},
StatusData = execute(Ep, Body, {AliceJID, Password}),
AliceJID = escalus_utils:jid_to_lower(escalus_client:short_jid(Alice)),
StatusData = execute_user(user_check_auth_body(), Alice, Config),
?assertUserAuth(AliceJID, 'AUTHORIZED', StatusData)
end).

admin_checks_auth(Config) ->
Ep = ?config(schema_endpoint, Config),
Body = #{query => "{ checkAuth }"},
StatusData = execute(Ep, Body, undefined),
?assertAdminAuth('UNAUTHORIZED', StatusData).
StatusData = execute(Ep, admin_check_auth_body(), undefined),
?assertAdminAuth(null, null, 'UNAUTHORIZED', StatusData).

auth_admin_checks_auth(Config) ->
StatusData = execute_auth(admin_check_auth_body(), Config),
?assertAdminAuth(null, 'ADMIN', 'AUTHORIZED', StatusData).

domain_admin_checks_auth(Config) ->
Ep = ?config(schema_endpoint, Config),
Opts = ?config(listener_opts, Config),
User = proplists:get_value(username, Opts),
Password = proplists:get_value(password, Opts),
Body = #{query => "{ checkAuth }"},
StatusData = execute(Ep, Body, {User, Password}),
?assertAdminAuth('AUTHORIZED', StatusData).
Res = execute(Ep, admin_check_auth_body(), undefined),
?assertAdminAuth(null, null, 'UNAUTHORIZED', Res).

auth_domain_admin_checks_auth(Config) ->
{Username, _} = ?config(domain_admin, Config),
Domain = escalus_utils:get_server(Username),
Res = execute_domain_auth(admin_check_auth_body(), Config),
?assertAdminAuth(Domain, 'DOMAIN_ADMIN', 'AUTHORIZED', Res).

%% Helpers

assert_auth(Auth, {Status, Data}) ->
?assertEqual({<<"200">>, <<"OK">>}, Status),
?assertMatch(#{<<"data">> := #{<<"checkAuth">> := Auth}}, Data).

user_password(User) ->
[{User, Props}] = escalus:get_users([User]),
proplists:get_value(password, Props).

get_graphiql_website(EpName) ->
Request =
#{port => graphql_helper:get_listener_port(EpName),
Expand All @@ -133,3 +151,12 @@ get_graphiql_website(EpName) ->
return_maps => true,
path => "/graphql"},
rest_helper:make_request(Request).

maybe_atom_to_bin(null) -> null;
maybe_atom_to_bin(X) -> atom_to_binary(X).

admin_check_auth_body() ->
#{query => "{ checkAuth { domain authType authStatus } }"}.

user_check_auth_body() ->
#{query => "{ checkAuth { username authStatus } }"}.
37 changes: 36 additions & 1 deletion big_tests/tests/graphql_domain_SUITE.erl
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,11 @@ domain_handler() ->
get_domains_by_host_type,
get_domain_details,
delete_domain,
get_domains_after_deletion].
get_domains_after_deletion,
set_domain_password,
set_nonexistent_domain_password,
delete_domain_password
].

init_per_suite(Config) ->
case mongoose_helper:is_rdbms_enabled(?HOST_TYPE) of
Expand Down Expand Up @@ -206,6 +210,29 @@ get_domains_after_deletion(Config) ->
ParsedResult = ok_result(<<"domains">>, <<"domainsByHostType">>, Result),
?assertEqual([], ParsedResult).

set_domain_password(Config) ->
Result = execute_auth(#{query => set_domain_password_call(),
variables => #{domain => domain_helper:domain(),
password => <<"secret">>},
operationName => <<"M1">>}, Config),
ParsedResult = ok_result(<<"domains">>, <<"setDomainPassword">>, Result),
?assertNotEqual(nomatch, binary:match(ParsedResult, <<"successfully">>)).

set_nonexistent_domain_password(Config) ->
Domain = <<"unknown-domain.com">>,
Result = execute_auth(#{query => set_domain_password_call(),
variables => #{domain => Domain,
password => <<"secret">>},
operationName => <<"M1">>}, Config),
domain_not_found_error_formatting(Result, Domain, <<"setDomainPassword">>).

delete_domain_password(Config) ->
Result = execute_auth(#{query => delete_domain_password_call(),
variables => #{domain => domain_helper:domain()},
operationName => <<"M1">>}, Config),
ParsedResult = ok_result(<<"domains">>, <<"deleteDomainPassword">>, Result),
?assertNotEqual(nomatch, binary:match(ParsedResult, <<"successfully">>)).

create_domain_call() ->
<<"mutation M1($domain: String!, $hostType: String!)
{domains
Expand Down Expand Up @@ -270,6 +297,14 @@ delete_domain_call() ->
}
}">>.

set_domain_password_call() ->
<<"mutation M1($domain: String!, $password: String!)"
"{ domains { setDomainPassword(domain: $domain, password: $password)} }">>.

delete_domain_password_call() ->
<<"mutation M1($domain: String!)"
"{ domains { deleteDomainPassword(domain: $domain)} }">>.

%% Helpers
ok_result(What1, What2, {{<<"200">>, <<"OK">>}, #{<<"data">> := Data}}) ->
maps:get(What2, maps:get(What1, Data)).
Expand Down
22 changes: 20 additions & 2 deletions big_tests/tests/graphql_helper.erl
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@

-import(distributed_helper, [mim/0, rpc/4]).

-export([execute/3, execute_auth/2, execute_user/3, get_listener_port/1, get_listener_config/1]).
-export([init_admin_handler/1]).
-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,
user_to_bin/1, user_to_full_bin/1, user_to_jid/1, user_to_lower_jid/1]).

Expand All @@ -30,6 +31,11 @@ execute_auth(Body, Config) ->
Password = proplists:get_value(password, Opts),
execute(Ep, Body, {User, Password}).

execute_domain_auth(Body, Config) ->
Ep = ?config(schema_endpoint, Config),
Creds = ?config(domain_admin, Config),
execute(Ep, Body, Creds).

execute_user(Body, User, Config) ->
Ep = ?config(schema_endpoint, Config),
Creds = make_creds(User),
Expand Down Expand Up @@ -57,6 +63,18 @@ init_admin_handler(Config) ->
ct:fail(<<"Admin credentials are not defined in config">>)
end.

init_domain_admin_handler(Config) ->
Domain = domain_helper:domain(),
Password = base16:encode(crypto:strong_rand_bytes(8)),
Creds = {<<"admin@", Domain/binary>>, Password},
ok = domain_helper:set_domain_password(mim(), Domain, Password),
[{domain_admin, Creds}, {schema_endpoint, domain_admin} | Config].

end_domain_admin_handler(Config) ->
{JID, _} = ?config(domain_admin, Config),
Domain = escalus_utils:get_server(JID),
domain_helper:delete_domain_password(mim(), Domain).

get_listener_opts(EpName) ->
#{handlers := Handlers} = get_listener_config(EpName),
[Opts2] = lists:filtermap(
Expand Down
2 changes: 1 addition & 1 deletion big_tests/tests/mongooseimctl_SUITE.erl
Original file line number Diff line number Diff line change
Expand Up @@ -1120,7 +1120,7 @@ stats_host(Config) ->
%%--------------------------------------------------------------------

can_execute_admin_queries_with_permissions(Config) ->
Query = "query { checkAuth }",
Query = "query { checkAuth { authStatus } }",
Res = mongooseimctl("graphql", [Query], Config),
?assertMatch({_, 0}, Res),
Data = element(1, Res),
Expand Down
58 changes: 58 additions & 0 deletions big_tests/tests/service_domain_db_SUITE.erl
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,12 @@ db_cases() -> [
db_cannot_enable_domain_with_unknown_host_type,
db_cannot_disable_domain_with_unknown_host_type,
db_domains_with_unknown_host_type_are_ignored_by_core,
db_can_insert_update_delete_dynamic_domain_password,
db_can_insert_update_delete_static_domain_password,
db_cannot_set_password_for_unknown_domain,
db_can_check_domain_password,
db_cannot_check_password_for_unknown_domain,
db_deleting_domain_deletes_domain_admin,
sql_select_from,
sql_find_gaps_between,
db_records_are_restored_on_mim_restart,
Expand Down Expand Up @@ -451,6 +457,46 @@ db_domains_with_unknown_host_type_are_ignored_by_core(_) ->
{ok, <<"type1">>} = get_host_type(mim(), <<"example.org">>), %% Counter-case
{error, not_found} = get_host_type(mim(), <<"example.com">>).

db_can_insert_update_delete_dynamic_domain_password(_) ->
Domain = <<"password-example.com">>,
ok = insert_domain(mim(), Domain, <<"type1">>),
sync(),
ok = set_domain_password(mim(), Domain, <<"rocky1">>),
ok = check_domain_password(mim(), Domain, <<"rocky1">>),
ok = set_domain_password(mim(), Domain, <<"rocky2">>),
ok = check_domain_password(mim(), Domain, <<"rocky2">>),
ok = delete_domain_password(mim(), Domain),
{error, not_found} = select_domain_admin(mim(), Domain).

db_can_insert_update_delete_static_domain_password(_) ->
StaticDomain = <<"example.cfg">>,
ok = set_domain_password(mim(), StaticDomain, <<"rocky1">>),
ok = check_domain_password(mim(), StaticDomain, <<"rocky1">>),
ok = set_domain_password(mim(), StaticDomain, <<"rocky2">>),
ok = check_domain_password(mim(), StaticDomain, <<"rocky2">>),
ok = delete_domain_password(mim(), StaticDomain),
{error, not_found} = select_domain_admin(mim(), StaticDomain).

db_cannot_set_password_for_unknown_domain(_) ->
{error, not_found} = set_domain_password(mim(), <<"unknown_domain">>, <<>>).

db_can_check_domain_password(_) ->
StaticDomain = <<"example.cfg">>,
ok = set_domain_password(mim(), StaticDomain, <<"myrock">>),
ok = check_domain_password(mim(), StaticDomain, <<"myrock">>),
{error, wrong_password} = check_domain_password(mim(), StaticDomain, <<"wrongrock">>).

db_cannot_check_password_for_unknown_domain(_) ->
{error, not_found} = check_domain_password(mim(), <<"unknown_domain">>, <<>>).

db_deleting_domain_deletes_domain_admin(_) ->
Domain = <<"password-del-example.db">>,
ok = insert_domain(mim(), Domain, <<"type1">>),
sync(),
ok = set_domain_password(mim(), Domain, <<"deleteme">>),
ok = delete_domain(mim(), Domain, <<"type1">>),
{error, not_found} = select_domain_admin(mim(), Domain).

sql_select_from(_) ->
ok = insert_domain(mim(), <<"example.db">>, <<"type1">>),
[{_, <<"example.db">>, <<"type1">>}] =
Expand Down Expand Up @@ -1065,6 +1111,18 @@ delete_domain(Node, Domain, HostType) ->
select_domain(Node, Domain) ->
rpc(Node, mongoose_domain_sql, select_domain, [Domain]).

check_domain_password(Node, Domain, Password) ->
rpc(Node, mongoose_domain_api, check_domain_password, [Domain, Password]).

set_domain_password(Node, Domain, Password) ->
rpc(Node, mongoose_domain_api, set_domain_password, [Domain, Password]).

delete_domain_password(Node, Domain) ->
rpc(Node, mongoose_domain_api, delete_domain_password, [Domain]).

select_domain_admin(Node, Domain) ->
rpc(Node, mongoose_domain_sql, select_domain_admin, [Domain]).

insert_full_event(Node, EventId, Domain) ->
rpc(Node, mongoose_domain_sql, insert_full_event, [EventId, Domain]).

Expand Down
18 changes: 18 additions & 0 deletions priv/graphql/schemas/admin/admin_auth_status.gql
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"Information about user request authorization"
type AdminAuthInfo{
"Authorized for a domain"
domain: String
"Authorization status"
authStatus: AuthStatus!
"Authorization as a "
authType: AuthType
}

enum AuthType{
""
DOMAIN_ADMIN
""
ADMIN
""
UNAUTHORIZED
}
2 changes: 1 addition & 1 deletion priv/graphql/schemas/admin/admin_schema.gql
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Only an authenticated admin can execute these queries.
"""
type AdminQuery{
"Check authorization status"
checkAuth: AuthStatus
checkAuth: AdminAuthInfo
"Domain management"
domains: DomainAdminQuery
"Account management"
Expand Down
Loading

0 comments on commit a6a323b

Please sign in to comment.