Просмотр исходного кода

chore(authn): add MongoDB backend tests

Ilya Averyanov 4 лет назад
Родитель
Сommit
390575eafb

+ 1 - 1
.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
 

+ 1 - 1
.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 \

+ 1 - 3
.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:

+ 2 - 0
.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 \

+ 2 - 0
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">> := _}) ->

+ 5 - 4
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, <<Salt/binary, Password/binary>>);

+ 0 - 22
apps/emqx_authn/test/emqx_authn_SUITE.erl

@@ -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).

+ 409 - 0
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).

+ 2 - 2
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,

+ 2 - 2
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,

+ 2 - 2
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,

+ 2 - 1
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);