228 lines
7.9 KiB
Erlang
228 lines
7.9 KiB
Erlang
%%--------------------------------------------------------------------
|
|
%% 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_mongodb).
|
|
|
|
-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('replica-set')
|
|
, hoconsc:t('sharded-cluster')
|
|
]}}
|
|
];
|
|
|
|
fields(standalone) ->
|
|
common_fields() ++ emqx_connector_mongo:fields(single);
|
|
|
|
fields('replica-set') ->
|
|
common_fields() ++ emqx_connector_mongo:fields(rs);
|
|
|
|
fields('sharded-cluster') ->
|
|
common_fields() ++ emqx_connector_mongo:fields(sharded).
|
|
|
|
common_fields() ->
|
|
[ {name, fun emqx_authn_schema:authenticator_name/1}
|
|
, {mechanism, {enum, ['password-based']}}
|
|
, {server_type, {enum, [mongodb]}}
|
|
, {collection, fun collection/1}
|
|
, {selector, fun selector/1}
|
|
, {password_hash_field, fun password_hash_field/1}
|
|
, {salt_field, fun salt_field/1}
|
|
, {password_hash_algorithm, fun password_hash_algorithm/1}
|
|
, {salt_position, fun salt_position/1}
|
|
].
|
|
|
|
collection(type) -> binary();
|
|
collection(nullable) -> false;
|
|
collection(_) -> undefined.
|
|
|
|
selector(type) -> map();
|
|
selector(nullable) -> false;
|
|
selector(_) -> undefined.
|
|
|
|
password_hash_field(type) -> binary();
|
|
password_hash_field(nullable) -> false;
|
|
password_hash_field(_) -> undefined.
|
|
|
|
salt_field(type) -> binary();
|
|
salt_field(nullable) -> true;
|
|
salt_field(_) -> 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(#{ selector := Selector
|
|
, '_unique' := Unique
|
|
} = Config) ->
|
|
NSelector = parse_selector(Selector),
|
|
State = maps:with([ collection
|
|
, password_hash_field
|
|
, salt_field
|
|
, password_hash_algorithm
|
|
, salt_position
|
|
, '_unique'], Config),
|
|
NState = State#{selector => NSelector},
|
|
case emqx_resource:create_local(Unique, emqx_connector_mongo, Config) of
|
|
{ok, _} ->
|
|
{ok, NState};
|
|
{error, already_created} ->
|
|
{ok, NState};
|
|
{error, Reason} ->
|
|
{error, Reason}
|
|
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,
|
|
#{ collection := Collection
|
|
, selector := Selector0
|
|
, '_unique' := Unique
|
|
} = State) ->
|
|
try
|
|
Selector1 = replace_placeholders(Selector0, Credential),
|
|
Selector2 = normalize_selector(Selector1),
|
|
case emqx_resource:query(Unique, {find_one, Collection, Selector2, #{}}) of
|
|
undefined -> ignore;
|
|
{error, Reason} ->
|
|
?LOG(error, "['~s'] Query failed: ~p", [Unique, Reason]),
|
|
ignore;
|
|
Doc ->
|
|
case check_password(Password, Doc, State) of
|
|
ok -> ok;
|
|
{error, {cannot_find_password_hash_field, PasswordHashField}} ->
|
|
?LOG(error, "['~s'] Can't find password hash field: ~s", [Unique, PasswordHashField]),
|
|
{error, bad_username_or_password};
|
|
{error, Reason} ->
|
|
{error, Reason}
|
|
end
|
|
end
|
|
catch
|
|
error:Error ->
|
|
?LOG(warning, "The following error occurred in '~s' during authentication: ~p", [Unique, Error]),
|
|
ignore
|
|
end.
|
|
|
|
destroy(#{'_unique' := Unique}) ->
|
|
_ = emqx_resource:remove_local(Unique),
|
|
ok.
|
|
|
|
%%------------------------------------------------------------------------------
|
|
%% Internal functions
|
|
%%------------------------------------------------------------------------------
|
|
|
|
parse_selector(Selector) ->
|
|
NSelector = emqx_json:encode(Selector),
|
|
Tokens = re:split(NSelector, "(" ++ ?RE_PLACEHOLDER ++ ")", [{return, binary}, group, trim]),
|
|
parse_selector(Tokens, []).
|
|
|
|
parse_selector([], Acc) ->
|
|
lists:reverse(Acc);
|
|
parse_selector([[Constant, Placeholder] | Tokens], Acc) ->
|
|
parse_selector(Tokens, [{placeholder, Placeholder}, {constant, Constant} | Acc]);
|
|
parse_selector([[Constant] | Tokens], Acc) ->
|
|
parse_selector(Tokens, [{constant, Constant} | Acc]).
|
|
|
|
replace_placeholders(Selector, 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, Selector).
|
|
|
|
normalize_selector(Selector) ->
|
|
emqx_json:decode(iolist_to_binary(Selector), [return_maps]).
|
|
|
|
check_password(undefined, _Selected, _State) ->
|
|
{error, bad_username_or_password};
|
|
check_password(Password,
|
|
Doc,
|
|
#{password_hash_algorithm := bcrypt,
|
|
password_hash_field := PasswordHashField}) ->
|
|
case maps:get(PasswordHashField, Doc, undefined) of
|
|
undefined ->
|
|
{error, {cannot_find_password_hash_field, PasswordHashField}};
|
|
Hash ->
|
|
case {ok, Hash} =:= bcrypt:hashpw(Password, Hash) of
|
|
true -> ok;
|
|
false -> {error, bad_username_or_password}
|
|
end
|
|
end;
|
|
check_password(Password,
|
|
Doc,
|
|
#{password_hash_algorithm := Algorithm,
|
|
password_hash_field := PasswordHashField,
|
|
salt_position := SaltPosition} = State) ->
|
|
case maps:get(PasswordHashField, Doc, undefined) of
|
|
undefined ->
|
|
{error, {cannot_find_password_hash_field, PasswordHashField}};
|
|
Hash ->
|
|
Salt = case maps:get(salt_field, State, undefined) of
|
|
undefined -> <<>>;
|
|
SaltField -> maps:get(SaltField, Doc, <<>>)
|
|
end,
|
|
case Hash =:= hash(Algorithm, Password, Salt, SaltPosition) of
|
|
true -> ok;
|
|
false -> {error, bad_username_or_password}
|
|
end
|
|
end.
|
|
|
|
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>>).
|