From 390575eafb8a508a5c9e199164a35b1fb1bf30e7 Mon Sep 17 00:00:00 2001 From: Ilya Averyanov Date: Mon, 22 Nov 2021 14:26:12 +0300 Subject: [PATCH] chore(authn): add MongoDB backend tests --- .ci/docker-compose-file/.env | 2 +- .ci/docker-compose-file/Makefile.local | 2 +- .../docker-compose-mongo-single-tcp.yaml | 4 +- .github/workflows/run_test_cases.yaml | 2 + apps/emqx_authn/src/emqx_authn_utils.erl | 2 + .../src/simple_authn/emqx_authn_mongodb.erl | 9 +- apps/emqx_authn/test/emqx_authn_SUITE.erl | 22 - .../test/emqx_authn_mongo_SUITE.erl | 409 ++++++++++++++++++ .../test/emqx_authn_mysql_SUITE.erl | 4 +- .../test/emqx_authn_pgsql_SUITE.erl | 4 +- .../test/emqx_authn_redis_SUITE.erl | 4 +- .../src/emqx_connector_mongo.erl | 3 +- 12 files changed, 429 insertions(+), 38 deletions(-) delete mode 100644 apps/emqx_authn/test/emqx_authn_SUITE.erl create mode 100644 apps/emqx_authn/test/emqx_authn_mongo_SUITE.erl diff --git a/.ci/docker-compose-file/.env b/.ci/docker-compose-file/.env index 8c9d056cc..ae3d12c64 100644 --- a/.ci/docker-compose-file/.env +++ b/.ci/docker-compose-file/.env @@ -1,6 +1,6 @@ MYSQL_TAG=8 REDIS_TAG=6 -MONGO_TAG=4 +MONGO_TAG=5 PGSQL_TAG=13 LDAP_TAG=2.4.50 diff --git a/.ci/docker-compose-file/Makefile.local b/.ci/docker-compose-file/Makefile.local index d5ef99d66..096da64c5 100644 --- a/.ci/docker-compose-file/Makefile.local +++ b/.ci/docker-compose-file/Makefile.local @@ -14,7 +14,7 @@ up: env \ MYSQL_TAG=8 \ REDIS_TAG=6 \ - MONGO_TAG=4 \ + MONGO_TAG=5 \ PGSQL_TAG=13 \ LDAP_TAG=2.4.50 \ docker-compose \ diff --git a/.ci/docker-compose-file/docker-compose-mongo-single-tcp.yaml b/.ci/docker-compose-file/docker-compose-mongo-single-tcp.yaml index 494b42ce4..5bba6147c 100644 --- a/.ci/docker-compose-file/docker-compose-mongo-single-tcp.yaml +++ b/.ci/docker-compose-file/docker-compose-mongo-single-tcp.yaml @@ -2,11 +2,9 @@ version: '3.9' services: mongo_server: - container_name: mongo + container_name: mongo image: mongo:${MONGO_TAG} restart: always - environment: - MONGO_INITDB_DATABASE: mqtt networks: - emqx_bridge ports: diff --git a/.github/workflows/run_test_cases.yaml b/.github/workflows/run_test_cases.yaml index d1f8bf577..49e94c322 100644 --- a/.github/workflows/run_test_cases.yaml +++ b/.github/workflows/run_test_cases.yaml @@ -55,12 +55,14 @@ jobs: - uses: actions/checkout@v2 - name: docker compose up env: + MONGO_TAG: 5 MYSQL_TAG: 8 PGSQL_TAG: 13 REDIS_TAG: 6 GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | docker-compose \ + -f .ci/docker-compose-file/docker-compose-mongo-single-tcp.yaml \ -f .ci/docker-compose-file/docker-compose-mysql-tcp.yaml \ -f .ci/docker-compose-file/docker-compose-pgsql-tcp.yaml \ -f .ci/docker-compose-file/docker-compose-redis-single-tcp.yaml \ diff --git a/apps/emqx_authn/src/emqx_authn_utils.erl b/apps/emqx_authn/src/emqx_authn_utils.erl index 56f485afc..2205d237d 100644 --- a/apps/emqx_authn/src/emqx_authn_utils.erl +++ b/apps/emqx_authn/src/emqx_authn_utils.erl @@ -93,6 +93,8 @@ is_superuser(#{<<"is_superuser">> := 0}) -> #{is_superuser => false}; is_superuser(#{<<"is_superuser">> := null}) -> #{is_superuser => false}; +is_superuser(#{<<"is_superuser">> := undefined}) -> + #{is_superuser => false}; is_superuser(#{<<"is_superuser">> := false}) -> #{is_superuser => false}; is_superuser(#{<<"is_superuser">> := _}) -> diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl index 40bd0c2c9..7e080dfee 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl @@ -115,6 +115,8 @@ create(#{selector := Selector} = Config) -> password_hash_algorithm, salt_position], Config), + #{password_hash_algorithm := Algorithm} = State, + ok = emqx_authn_utils:ensure_apps_started(Algorithm), ResourceId = emqx_authn_utils:make_resource_id(?MODULE), NState = State#{ selector => NSelector, @@ -155,7 +157,7 @@ authenticate(#{password := Password} = Credential, Doc -> case check_password(Password, Doc, State) of ok -> - {ok, #{is_superuser => is_superuser(Doc, State)}}; + {ok, is_superuser(Doc, State)}; {error, {cannot_find_password_hash_field, PasswordHashField}} -> ?SLOG(error, #{msg => "cannot_find_password_hash_field", resource => ResourceId, @@ -234,9 +236,8 @@ check_password(Password, end. is_superuser(Doc, #{is_superuser_field := IsSuperuserField}) -> - maps:get(IsSuperuserField, Doc, false); -is_superuser(_, _) -> - false. + IsSuperuser = maps:get(IsSuperuserField, Doc, false), + emqx_authn_utils:is_superuser(#{<<"is_superuser">> => IsSuperuser}). hash(Algorithm, Password, Salt, prefix) -> emqx_passwd:hash(Algorithm, <>); diff --git a/apps/emqx_authn/test/emqx_authn_SUITE.erl b/apps/emqx_authn/test/emqx_authn_SUITE.erl deleted file mode 100644 index d3704679f..000000000 --- a/apps/emqx_authn/test/emqx_authn_SUITE.erl +++ /dev/null @@ -1,22 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqx_authn_SUITE). - --compile(export_all). --compile(nowarn_export_all). - -all() -> emqx_common_test_helpers:all(?MODULE). diff --git a/apps/emqx_authn/test/emqx_authn_mongo_SUITE.erl b/apps/emqx_authn/test/emqx_authn_mongo_SUITE.erl new file mode 100644 index 000000000..e6ae1706a --- /dev/null +++ b/apps/emqx_authn/test/emqx_authn_mongo_SUITE.erl @@ -0,0 +1,409 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_authn_mongo_SUITE). + +-compile(nowarn_export_all). +-compile(export_all). + +-include("emqx_authn.hrl"). +-include_lib("eunit/include/eunit.hrl"). +-include_lib("common_test/include/ct.hrl"). + + +-define(MONGO_HOST, "mongo"). +-define(MONGO_PORT, 27017). +-define(MONGO_CLIENT, 'emqx_authn_mongo_SUITE_client'). + +-define(PATH, [authentication]). + +all() -> + emqx_common_test_helpers:all(?MODULE). + + +init_per_testcase(_TestCase, Config) -> + emqx_authentication:initialize_authentication(?GLOBAL, []), + emqx_authn_test_lib:delete_authenticators( + [authentication], + ?GLOBAL), + {ok, _} = mc_worker_api:connect(mongo_config()), + Config. + +end_per_testcase(_TestCase, _Config) -> + ok = mc_worker_api:disconnect(?MONGO_CLIENT). + + +init_per_suite(Config) -> + case emqx_authn_test_lib:is_tcp_server_available(?MONGO_HOST, ?MONGO_PORT) of + true -> + ok = emqx_common_test_helpers:start_apps([emqx_authn]), + ok = start_apps([emqx_resource, emqx_connector]), + Config; + false -> + {skip, no_mongo} + end. + +end_per_suite(_Config) -> + emqx_authn_test_lib:delete_authenticators( + [authentication], + ?GLOBAL), + ok = stop_apps([emqx_resource, emqx_connector]), + ok = emqx_common_test_helpers:stop_apps([emqx_authn]). + +%%------------------------------------------------------------------------------ +%% Tests +%%------------------------------------------------------------------------------ + +t_create(_Config) -> + AuthConfig = raw_mongo_auth_config(), + + {ok, _} = emqx:update_config( + ?PATH, + {create_authenticator, ?GLOBAL, AuthConfig}), + + {ok, [#{provider := emqx_authn_mongodb}]} = emqx_authentication:list_authenticators(?GLOBAL). + +t_create_invalid(_Config) -> + AuthConfig = raw_mongo_auth_config(), + + InvalidConfigs = + [ + AuthConfig#{mongo_type => <<"unknown">>}, + AuthConfig#{selector => <<"{ \"username\": \"${username}\" }">>} + ], + + lists:foreach( + fun(Config) -> + {error, _} = emqx:update_config( + ?PATH, + {create_authenticator, ?GLOBAL, Config}), + + {ok, []} = emqx_authentication:list_authenticators(?GLOBAL) + end, + InvalidConfigs). + +t_authenticate(_Config) -> + ok = init_seeds(), + ok = lists:foreach( + fun(Sample) -> + ct:pal("test_user_auth sample: ~p", [Sample]), + test_user_auth(Sample) + end, + user_seeds()), + ok = drop_seeds(). + +test_user_auth(#{credentials := Credentials0, + config_params := SpecificConfigParams, + result := Result}) -> + AuthConfig = maps:merge(raw_mongo_auth_config(), SpecificConfigParams), + + {ok, _} = emqx:update_config( + ?PATH, + {create_authenticator, ?GLOBAL, AuthConfig}), + + Credentials = Credentials0#{ + listener => 'tcp:default', + protocol => mqtt + }, + ?assertEqual(Result, emqx_access_control:authenticate(Credentials)), + + emqx_authn_test_lib:delete_authenticators( + [authentication], + ?GLOBAL). + +t_destroy(_Config) -> + ok = init_seeds(), + AuthConfig = raw_mongo_auth_config(), + + {ok, _} = emqx:update_config( + ?PATH, + {create_authenticator, ?GLOBAL, AuthConfig}), + + {ok, [#{provider := emqx_authn_mongodb, state := State}]} + = emqx_authentication:list_authenticators(?GLOBAL), + + {ok, _} = emqx_authn_mongodb:authenticate( + #{username => <<"plain">>, + password => <<"plain">> + }, + State), + + emqx_authn_test_lib:delete_authenticators( + [authentication], + ?GLOBAL), + + % Authenticator should not be usable anymore + ?assertException( + error, + _, + emqx_authn_mongodb:authenticate( + #{username => <<"plain">>, + password => <<"plain">> + }, + State)), + + ok = drop_seeds(). + +t_update(_Config) -> + ok = init_seeds(), + CorrectConfig = raw_mongo_auth_config(), + IncorrectConfig = + CorrectConfig#{selector => #{<<"wrongfield">> => <<"wrongvalue">>}}, + + {ok, _} = emqx:update_config( + ?PATH, + {create_authenticator, ?GLOBAL, IncorrectConfig}), + + {error, not_authorized} = emqx_access_control:authenticate( + #{username => <<"plain">>, + password => <<"plain">>, + listener => 'tcp:default', + protocol => mqtt + }), + + % We update with config with correct selector, provider should update and work properly + {ok, _} = emqx:update_config( + ?PATH, + {update_authenticator, ?GLOBAL, <<"password-based:mongodb">>, CorrectConfig}), + + {ok,_} = emqx_access_control:authenticate( + #{username => <<"plain">>, + password => <<"plain">>, + listener => 'tcp:default', + protocol => mqtt + }), + ok = drop_seeds(). + +t_is_superuser(_Config) -> + Config = raw_mongo_auth_config(), + {ok, _} = emqx:update_config( + ?PATH, + {create_authenticator, ?GLOBAL, Config}), + + Checks = [ + {<<"0">>, false}, + {<<"">>, false}, + {null, false}, + {false, false}, + {0, false}, + + {<<"1">>, true}, + {<<"val">>, true}, + {1, true}, + {123, true}, + {true, true} + ], + + lists:foreach(fun test_is_superuser/1, Checks). + +test_is_superuser({Value, ExpectedValue}) -> + {true, _} = mc_worker_api:delete(?MONGO_CLIENT, <<"users">>, #{}), + + UserData = #{ + username => <<"user">>, + password_hash => <<"plainsalt">>, + salt => <<"salt">>, + is_superuser => Value + }, + + {{true, _}, _} = mc_worker_api:insert(?MONGO_CLIENT, <<"users">>, [UserData]), + + Credentials = #{ + listener => 'tcp:default', + protocol => mqtt, + username => <<"user">>, + password => <<"plain">> + }, + + ?assertEqual( + {ok, #{is_superuser => ExpectedValue}}, + emqx_access_control:authenticate(Credentials)). + +%%------------------------------------------------------------------------------ +%% Helpers +%%------------------------------------------------------------------------------ + +raw_mongo_auth_config() -> + #{ + mechanism => <<"password-based">>, + password_hash_algorithm => <<"plain">>, + salt_position => <<"suffix">>, + enable => <<"true">>, + + backend => <<"mongodb">>, + mongo_type => <<"single">>, + database => <<"mqtt">>, + collection => <<"users">>, + server => mongo_server(), + + selector => #{<<"username">> => <<"${username}">>}, + password_hash_field => <<"password_hash">>, + salt_field => <<"salt">>, + is_superuser_field => <<"is_superuser">> + }. + +user_seeds() -> + [#{data => #{ + username => <<"plain">>, + password_hash => <<"plainsalt">>, + salt => <<"salt">>, + is_superuser => <<"1">> + }, + credentials => #{ + username => <<"plain">>, + password => <<"plain">> + }, + config_params => #{ + }, + result => {ok,#{is_superuser => true}} + }, + + #{data => #{ + username => <<"md5">>, + password_hash => <<"9b4d0c43d206d48279e69b9ad7132e22">>, + salt => <<"salt">>, + is_superuser => <<"0">> + }, + credentials => #{ + username => <<"md5">>, + password => <<"md5">> + }, + config_params => #{ + password_hash_algorithm => <<"md5">>, + salt_position => <<"suffix">> + }, + result => {ok,#{is_superuser => false}} + }, + + #{data => #{ + username => <<"sha256">>, + password_hash => <<"ac63a624e7074776d677dd61a003b8c803eb11db004d0ec6ae032a5d7c9c5caf">>, + salt => <<"salt">>, + is_superuser => 1 + }, + credentials => #{ + clientid => <<"sha256">>, + password => <<"sha256">> + }, + config_params => #{ + selector => #{<<"username">> => <<"${clientid}">>}, + password_hash_algorithm => <<"sha256">>, + salt_position => <<"prefix">> + }, + result => {ok,#{is_superuser => true}} + }, + + #{data => #{ + username => <<"bcrypt">>, + password_hash => <<"$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u">>, + salt => <<"$2b$12$wtY3h20mUjjmeaClpqZVve">>, + is_superuser => 0 + }, + credentials => #{ + username => <<"bcrypt">>, + password => <<"bcrypt">> + }, + config_params => #{ + password_hash_algorithm => <<"bcrypt">>, + salt_position => <<"suffix">> % should be ignored + }, + result => {ok,#{is_superuser => false}} + }, + + #{data => #{ + username => <<"bcrypt0">>, + password_hash => <<"$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u">>, + salt => <<"$2b$12$wtY3h20mUjjmeaClpqZVve">>, + is_superuser => <<"0">> + }, + credentials => #{ + username => <<"bcrypt0">>, + password => <<"bcrypt">> + }, + config_params => #{ + % clientid variable & username credentials + selector => #{<<"username">> => <<"${clientid}">>}, + password_hash_algorithm => <<"bcrypt">>, + salt_position => <<"suffix">> + }, + result => {error,not_authorized} + }, + + #{data => #{ + username => <<"bcrypt1">>, + password_hash => <<"$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u">>, + salt => <<"$2b$12$wtY3h20mUjjmeaClpqZVve">>, + is_superuser => <<"0">> + }, + credentials => #{ + username => <<"bcrypt1">>, + password => <<"bcrypt">> + }, + config_params => #{ + selector => #{<<"userid">> => <<"${clientid}">>}, + password_hash_algorithm => <<"bcrypt">>, + salt_position => <<"suffix">> + }, + result => {error,not_authorized} + }, + + #{data => #{ + username => <<"bcrypt2">>, + password_hash => <<"$2b$12$wtY3h20mUjjmeaClpqZVveDWGlHzCGsvuThMlneGHA7wVeFYyns2u">>, + salt => <<"$2b$12$wtY3h20mUjjmeaClpqZVve">>, + is_superuser => <<"0">> + }, + credentials => #{ + username => <<"bcrypt2">>, + % Wrong password + password => <<"wrongpass">> + }, + config_params => #{ + password_hash_algorithm => <<"bcrypt">>, + salt_position => <<"suffix">> + }, + result => {error,bad_username_or_password} + } + ]. + +init_seeds() -> + Users = [Values || #{data := Values} <- user_seeds()], + {{true, _}, _} = mc_worker_api:insert(?MONGO_CLIENT, <<"users">>, Users), + ok. + +drop_seeds() -> + {true, _} = mc_worker_api:delete(?MONGO_CLIENT, <<"users">>, #{}), + ok. + +mongo_server() -> + iolist_to_binary( + io_lib:format( + "~s:~b", + [?MONGO_HOST, ?MONGO_PORT])). + +mongo_config() -> + [ + {database, <<"mqtt">>}, + {host, ?MONGO_HOST}, + {port, ?MONGO_PORT}, + {register, ?MONGO_CLIENT} + ]. + +start_apps(Apps) -> + lists:foreach(fun application:ensure_all_started/1, Apps). + +stop_apps(Apps) -> + lists:foreach(fun application:stop/1, Apps). diff --git a/apps/emqx_authn/test/emqx_authn_mysql_SUITE.erl b/apps/emqx_authn/test/emqx_authn_mysql_SUITE.erl index 9073dd38a..48569ed36 100644 --- a/apps/emqx_authn/test/emqx_authn_mysql_SUITE.erl +++ b/apps/emqx_authn/test/emqx_authn_mysql_SUITE.erl @@ -117,9 +117,9 @@ t_authenticate(_Config) -> user_seeds()). test_user_auth(#{credentials := Credentials0, - config_params := SpecificConfgParams, + config_params := SpecificConfigParams, result := Result}) -> - AuthConfig = maps:merge(raw_mysql_auth_config(), SpecificConfgParams), + AuthConfig = maps:merge(raw_mysql_auth_config(), SpecificConfigParams), {ok, _} = emqx:update_config( ?PATH, diff --git a/apps/emqx_authn/test/emqx_authn_pgsql_SUITE.erl b/apps/emqx_authn/test/emqx_authn_pgsql_SUITE.erl index 08bb2ee2e..e3848afbc 100644 --- a/apps/emqx_authn/test/emqx_authn_pgsql_SUITE.erl +++ b/apps/emqx_authn/test/emqx_authn_pgsql_SUITE.erl @@ -117,9 +117,9 @@ t_authenticate(_Config) -> user_seeds()). test_user_auth(#{credentials := Credentials0, - config_params := SpecificConfgParams, + config_params := SpecificConfigParams, result := Result}) -> - AuthConfig = maps:merge(raw_pgsql_auth_config(), SpecificConfgParams), + AuthConfig = maps:merge(raw_pgsql_auth_config(), SpecificConfigParams), {ok, _} = emqx:update_config( ?PATH, diff --git a/apps/emqx_authn/test/emqx_authn_redis_SUITE.erl b/apps/emqx_authn/test/emqx_authn_redis_SUITE.erl index 8669080b0..be1ae6d13 100644 --- a/apps/emqx_authn/test/emqx_authn_redis_SUITE.erl +++ b/apps/emqx_authn/test/emqx_authn_redis_SUITE.erl @@ -124,9 +124,9 @@ t_authenticate(_Config) -> user_seeds()). test_user_auth(#{credentials := Credentials0, - config_params := SpecificConfgParams, + config_params := SpecificConfigParams, result := Result}) -> - AuthConfig = maps:merge(raw_redis_auth_config(), SpecificConfgParams), + AuthConfig = maps:merge(raw_redis_auth_config(), SpecificConfigParams), {ok, _} = emqx:update_config( ?PATH, diff --git a/apps/emqx_connector/src/emqx_connector_mongo.erl b/apps/emqx_connector/src/emqx_connector_mongo.erl index 11eac9c91..6cee46d75 100644 --- a/apps/emqx_connector/src/emqx_connector_mongo.erl +++ b/apps/emqx_connector/src/emqx_connector_mongo.erl @@ -181,12 +181,13 @@ health_check(PoolName) -> %% =================================================================== connect(Opts) -> - Type = proplists:get_value(mongo_type, Opts, single), + Type = proplists:get_value(type, Opts, single), Hosts = proplists:get_value(hosts, Opts, []), Options = proplists:get_value(options, Opts, []), WorkerOptions = proplists:get_value(worker_options, Opts, []), mongo_api:connect(Type, Hosts, Options, WorkerOptions). + mongo_query(Conn, find, Collection, Selector, Projector) -> mongo_api:find(Conn, Collection, Selector, Projector);