Преглед изворни кода

feat(authn redis): support authn with redis

zhouzb пре 4 година
родитељ
комит
3761db0525

+ 2 - 0
.ci/docker-compose-file/docker-compose-redis-single-tcp.yaml

@@ -4,6 +4,8 @@ services:
   redis_server:
   redis_server:
     container_name: redis 
     container_name: redis 
     image: redis:${REDIS_TAG}
     image: redis:${REDIS_TAG}
+    ports:
+      - "6379:6379"
     command:
     command:
       - redis-server
       - redis-server
       - "--bind 0.0.0.0 ::"
       - "--bind 0.0.0.0 ::"

+ 11 - 0
apps/emqx_authn/etc/emqx_authn.conf

@@ -21,6 +21,17 @@ authentication: {
         #     salt_field: salt
         #     salt_field: salt
         #     password_hash_algorithm: sha256
         #     password_hash_algorithm: sha256
         #     salt_position: prefix
         #     salt_position: prefix
+        # },
+        # {
+        #     name: "authenticator 3"
+        #     mechanism: password-based
+        #     server_type: redis
+        #     server: "127.0.0.1:6379"
+        #     password: "public"
+        #     database: 0
+        #     query: "HMGET ${mqtt-username} password_hash salt"
+        #     password_hash_algorithm: sha256
+        #     salt_position: prefix
         # }
         # }
     ]
     ]
 }
 }

+ 2 - 0
apps/emqx_authn/src/emqx_authn.erl

@@ -325,6 +325,8 @@ authenticator_provider(#{mechanism := 'password-based', server_type := 'pgsql'})
     emqx_authn_pgsql;
     emqx_authn_pgsql;
 authenticator_provider(#{mechanism := 'password-based', server_type := 'mongodb'}) ->
 authenticator_provider(#{mechanism := 'password-based', server_type := 'mongodb'}) ->
     emqx_authn_mongodb;
     emqx_authn_mongodb;
+authenticator_provider(#{mechanism := 'password-based', server_type := 'redis'}) ->
+    emqx_authn_redis;
 authenticator_provider(#{mechanism := 'password-based', server_type := 'http-server'}) ->
 authenticator_provider(#{mechanism := 'password-based', server_type := 'http-server'}) ->
     emqx_authn_http;
     emqx_authn_http;
 authenticator_provider(#{mechanism := jwt}) ->
 authenticator_provider(#{mechanism := jwt}) ->

+ 4 - 1
apps/emqx_authn/src/emqx_authn_schema.erl

@@ -49,7 +49,10 @@ authenticators(type) ->
                           , hoconsc:ref(emqx_authn_pgsql, config)
                           , hoconsc:ref(emqx_authn_pgsql, config)
                           , hoconsc:ref(emqx_authn_mongodb, standalone)
                           , hoconsc:ref(emqx_authn_mongodb, standalone)
                           , hoconsc:ref(emqx_authn_mongodb, 'replica-set')
                           , hoconsc:ref(emqx_authn_mongodb, 'replica-set')
-                          , hoconsc:ref(emqx_authn_mongodb, sharded)
+                          , hoconsc:ref(emqx_authn_mongodb, 'sharded-cluster')
+                          , hoconsc:ref(emqx_authn_redis, standalone)
+                          , hoconsc:ref(emqx_authn_redis, cluster)
+                          , hoconsc:ref(emqx_authn_redis, sentinel)
                           , hoconsc:ref(emqx_authn_http, get)
                           , hoconsc:ref(emqx_authn_http, get)
                           , hoconsc:ref(emqx_authn_http, post)
                           , hoconsc:ref(emqx_authn_http, post)
                           , hoconsc:ref(emqx_authn_jwt, 'hmac-based')
                           , hoconsc:ref(emqx_authn_jwt, 'hmac-based')

+ 5 - 0
apps/emqx_authn/src/emqx_authn_utils.erl

@@ -18,6 +18,7 @@
 
 
 -export([ replace_placeholders/2
 -export([ replace_placeholders/2
         , replace_placeholder/2
         , replace_placeholder/2
+        , hash/4
         , gen_salt/0
         , gen_salt/0
         , bin/1
         , bin/1
         ]).
         ]).
@@ -54,6 +55,10 @@ replace_placeholder(<<"${cert-common-name}">>, Credential) ->
 replace_placeholder(Constant, _) ->
 replace_placeholder(Constant, _) ->
     Constant.
     Constant.
 
 
+hash(Algorithm, Password, Salt, prefix) ->
+    emqx_passwd:hash(Algorithm, <<Salt/binary, Password/binary>>);
+hash(Algorithm, Password, Salt, suffix) ->
+    emqx_passwd:hash(Algorithm, <<Password/binary, Salt/binary>>).
 
 
 gen_salt() ->
 gen_salt() ->
     <<X:128/big-unsigned-integer>> = crypto:strong_rand_bytes(16),
     <<X:128/big-unsigned-integer>> = crypto:strong_rand_bytes(16),

+ 2 - 2
apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl

@@ -41,7 +41,7 @@ structs() -> [""].
 fields("") ->
 fields("") ->
     [ {config, {union, [ hoconsc:t(standalone)
     [ {config, {union, [ hoconsc:t(standalone)
                        , hoconsc:t('replica-set')
                        , hoconsc:t('replica-set')
-                       , hoconsc:t(sharded)
+                       , hoconsc:t('sharded-cluster')
                        ]}}
                        ]}}
     ];
     ];
 
 
@@ -51,7 +51,7 @@ fields(standalone) ->
 fields('replica-set') ->
 fields('replica-set') ->
     common_fields() ++ emqx_connector_mongo:fields(rs);
     common_fields() ++ emqx_connector_mongo:fields(rs);
 
 
-fields(sharded) ->
+fields('sharded-cluster') ->
     common_fields() ++ emqx_connector_mongo:fields(sharded).
     common_fields() ++ emqx_connector_mongo:fields(sharded).
 
 
 common_fields() ->
 common_fields() ->

+ 1 - 5
apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl

@@ -146,11 +146,7 @@ check_password(Password,
                #{password_hash_algorithm := Algorithm,
                #{password_hash_algorithm := Algorithm,
                  salt_position := SaltPosition}) ->
                  salt_position := SaltPosition}) ->
     Salt = maps:get(salt, Selected, <<>>),
     Salt = maps:get(salt, Selected, <<>>),
-    Hash0 = case SaltPosition of
-                prefix -> emqx_passwd:hash(Algorithm, <<Salt/binary, Password/binary>>);
-                suffix -> emqx_passwd:hash(Algorithm, <<Password/binary, Salt/binary>>)
-            end,
-    case Hash0 =:= Hash of
+    case Hash =:= emqx_authn_utils:hash(Algorithm, Password, Salt, SaltPosition) of
         true -> ok;
         true -> ok;
         false -> {error, bad_username_or_password}
         false -> {error, bad_username_or_password}
     end.
     end.

+ 1 - 5
apps/emqx_authn/src/simple_authn/emqx_authn_pgsql.erl

@@ -132,11 +132,7 @@ check_password(Password,
                #{password_hash_algorithm := Algorithm,
                #{password_hash_algorithm := Algorithm,
                  salt_position := SaltPosition}) ->
                  salt_position := SaltPosition}) ->
     Salt = maps:get(salt, Selected, <<>>),
     Salt = maps:get(salt, Selected, <<>>),
-    Hash0 = case SaltPosition of
-                prefix -> emqx_passwd:hash(Algorithm, <<Salt/binary, Password/binary>>);
-                suffix -> emqx_passwd:hash(Algorithm, <<Password/binary, Salt/binary>>)
-            end,
-    case Hash0 =:= Hash of
+    case Hash =:= emqx_authn_utils:hash(Algorithm, Password, Salt, SaltPosition) of
         true -> ok;
         true -> ok;
         false -> {error, bad_username_or_password}
         false -> {error, bad_username_or_password}
     end.
     end.

+ 222 - 0
apps/emqx_authn/src/simple_authn/emqx_authn_redis.erl

@@ -0,0 +1,222 @@
+%%--------------------------------------------------------------------
+%% 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_redis).
+
+-include("emqx_authn.hrl").
+-include_lib("emqx/include/logger.hrl").
+-include_lib("typerefl/include/types.hrl").
+
+-behaviour(hocon_schema).
+
+-export([ structs/0
+        , fields/1
+        ]).
+
+-export([ create/1
+        , update/2
+        , authenticate/2
+        , destroy/1
+        ]).
+
+%%------------------------------------------------------------------------------
+%% Hocon Schema
+%%------------------------------------------------------------------------------
+
+structs() -> [""].
+
+fields("") ->
+    [ {config, {union, [ hoconsc:t(standalone)
+                       , hoconsc:t(cluster)
+                       , hoconsc:t(sentinel)
+                       ]}}
+    ];
+
+fields(standalone) ->
+    common_fields() ++ emqx_connector_redis:fields(single);
+
+fields(cluster) ->
+    common_fields() ++ emqx_connector_redis:fields(cluster);
+
+fields(sentinel) ->
+    common_fields() ++ emqx_connector_redis:fields(sentinel).
+
+common_fields() ->
+    [ {name,                    fun emqx_authn_schema:authenticator_name/1}
+    , {mechanism,               {enum, ['password-based']}}
+    , {server_type,             {enum, [redis]}}
+    , {query,                   fun query/1}
+    , {password_hash_algorithm, fun password_hash_algorithm/1}
+    , {salt_position,           fun salt_position/1}
+    ].
+
+query(type) -> string();
+query(nullable) -> false;
+query(_) -> undefined.
+
+password_hash_algorithm(type) -> {enum, [plain, md5, sha, sha256, sha512, bcrypt]};
+password_hash_algorithm(default) -> sha256;
+password_hash_algorithm(_) -> undefined.
+
+salt_position(type) -> {enum, [prefix, suffix]};
+salt_position(default) -> prefix;
+salt_position(_) -> undefined.
+
+%%------------------------------------------------------------------------------
+%% APIs
+%%------------------------------------------------------------------------------
+
+create(#{ query := Query
+        , '_unique' := Unique
+        } = Config) ->
+    try
+        NQuery = parse_query(Query),
+        State = maps:with([ password_hash_algorithm
+                          , salt_position
+                          , '_unique'], Config),
+        NState = State#{query => NQuery},
+        case emqx_resource:create_local(Unique, emqx_connector_redis, Config) of
+            {ok, _} ->
+                {ok, NState};
+            {error, already_created} ->
+                {ok, NState};
+            {error, Reason} ->
+                {error, Reason}
+        end
+    catch
+        error:{unsupported_query, Query} ->
+            {error, {unsupported_query, Query}};
+        error:missing_password_hash ->
+            {error, missing_password_hash};
+        error:{unsupported_field, Field} ->
+            {error, {unsupported_field, Field}}
+    end.
+
+update(Config, State) ->
+    case create(Config) of
+        {ok, NewState} ->
+            ok = destroy(State),
+            {ok, NewState};
+        {error, Reason} ->
+            {error, Reason}
+    end.
+
+authenticate(#{auth_method := _}, _) ->
+    ignore;
+authenticate(#{password := Password} = Credential,
+             #{ query := {Command, Key, Fields}
+              , '_unique' := Unique
+              } = State) ->
+    try
+        NKey = binary_to_list(iolist_to_binary(replace_placeholders(Key, Credential))),
+        case emqx_resource:query(Unique, {cmd, [Command, NKey | Fields]}) of
+            {ok, Values} ->
+                check_password(Password, merge(Fields, Values), State);
+            {error, Reason} ->
+                ?LOG(error, "['~s'] Query failed: ~p", [Unique, Reason]),
+                ignore
+        end
+    catch
+        error:{cannot_get_variable, Placeholder} ->
+            ?LOG(warning, "The following error occurred in '~s' during authentication: ~p", [Unique, {cannot_get_variable, Placeholder}]),
+            ignore
+    end.
+
+destroy(#{'_unique' := Unique}) ->
+    _ = emqx_resource:remove_local(Unique),
+    ok.
+
+%%------------------------------------------------------------------------------
+%% Internal functions
+%%------------------------------------------------------------------------------
+
+%% Only support HGET and HMGET
+parse_query(Query) ->
+    case string:tokens(Query, " ") of
+        [Command, Key, Field | Fields] when Command =:= "HGET" orelse Command =:= "HMGET" ->
+            NFields = [Field | Fields],
+            check_fields(NFields),
+            NKey = parse_key(Key),
+            {Command, NKey, NFields};
+        _ ->
+            error({unsupported_query, Query})
+    end.
+
+check_fields(Fields) ->
+    check_fields(Fields, false).
+
+check_fields([], false) ->
+    error(missing_password_hash);
+check_fields([], true) ->
+    ok;
+check_fields(["password_hash" | More], false) ->
+    check_fields(More, true);
+check_fields(["salt" | More], HasPassHash) ->
+    check_fields(More, HasPassHash);
+% check_fields(["is_superuser" | More], HasPassHash) ->
+%     check_fields(More, HasPassHash);
+check_fields([Field | _], _) ->
+    error({unsupported_field, Field}).
+    
+parse_key(Key) ->
+    Tokens = re:split(Key, "(" ++ ?RE_PLACEHOLDER ++ ")", [{return, binary}, group, trim]),
+    parse_key(Tokens, []).
+
+parse_key([], Acc) ->
+    lists:reverse(Acc);
+parse_key([[Constant, Placeholder] | Tokens], Acc) ->
+    parse_key(Tokens, [{placeholder, Placeholder}, {constant, Constant} | Acc]);
+parse_key([[Constant] | Tokens], Acc) ->
+    parse_key(Tokens, [{constant, Constant} | Acc]).
+
+replace_placeholders(Key, Credential) ->
+    lists:map(fun({constant, Constant}) ->
+                  Constant;
+                 ({placeholder, Placeholder}) ->
+                  case emqx_authn_utils:replace_placeholder(Placeholder, Credential) of
+                      undefined -> error({cannot_get_variable, Placeholder});
+                      Value -> Value
+                  end
+              end, Key).
+
+merge(Fields, Value) when not is_list(Value) ->
+    merge(Fields, [Value]);
+merge(Fields, Values) ->
+    maps:from_list(
+        lists:filter(fun({_, V}) ->
+                         V =/= undefined
+                     end, lists:zip(Fields, Values))).
+
+check_password(undefined, _Selected, _State) ->
+    {error, bad_username_or_password};
+check_password(Password,
+               #{"password_hash" := PasswordHash},
+               #{password_hash_algorithm := bcrypt}) ->
+    case {ok, PasswordHash} =:= bcrypt:hashpw(Password, PasswordHash) of
+        true -> ok;
+        false -> {error, bad_username_or_password}
+    end;
+check_password(Password,
+               #{"password_hash" := PasswordHash} = Selected,
+               #{password_hash_algorithm := Algorithm,
+                 salt_position := SaltPosition}) ->
+    Salt = maps:get("salt", Selected, <<>>),
+    case PasswordHash =:= emqx_authn_utils:hash(Algorithm, Password, Salt, SaltPosition) of
+        true -> ok;
+        false -> {error, bad_username_or_password}
+    end;
+check_password(_Password, _Selected, _State) ->
+    ignore.

+ 1 - 1
rebar.config

@@ -43,7 +43,7 @@
 
 
 {deps,
 {deps,
     [ {gpb, "4.11.2"} %% gpb only used to build, but not for release, pin it here to avoid fetching a wrong version due to rebar plugins scattered in all the deps
     [ {gpb, "4.11.2"} %% gpb only used to build, but not for release, pin it here to avoid fetching a wrong version due to rebar plugins scattered in all the deps
-    , {ehttpc, {git, "https://github.com/emqx/ehttpc", {tag, "0.1.8"}}}
+    , {ehttpc, {git, "https://github.com/emqx/ehttpc", {tag, "0.1.9"}}}
     , {gproc, {git, "https://github.com/uwiger/gproc", {tag, "0.8.0"}}}
     , {gproc, {git, "https://github.com/uwiger/gproc", {tag, "0.8.0"}}}
     , {jiffy, {git, "https://github.com/emqx/jiffy", {tag, "1.0.5"}}}
     , {jiffy, {git, "https://github.com/emqx/jiffy", {tag, "1.0.5"}}}
     , {cowboy, {git, "https://github.com/emqx/cowboy", {tag, "2.8.2"}}}
     , {cowboy, {git, "https://github.com/emqx/cowboy", {tag, "2.8.2"}}}