268 lines
8.1 KiB
Erlang
268 lines
8.1 KiB
Erlang
%%--------------------------------------------------------------------
|
|
%% Copyright (c) 2020-2023 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("hocon/include/hoconsc.hrl").
|
|
|
|
-behaviour(hocon_schema).
|
|
-behaviour(emqx_authentication).
|
|
|
|
-export([
|
|
namespace/0,
|
|
tags/0,
|
|
roots/0,
|
|
fields/1,
|
|
desc/1
|
|
]).
|
|
|
|
-export([
|
|
refs/0,
|
|
union_member_selector/1,
|
|
create/2,
|
|
update/2,
|
|
authenticate/2,
|
|
destroy/1
|
|
]).
|
|
|
|
%%------------------------------------------------------------------------------
|
|
%% Hocon Schema
|
|
%%------------------------------------------------------------------------------
|
|
|
|
namespace() -> "authn".
|
|
|
|
tags() ->
|
|
[<<"Authentication">>].
|
|
|
|
%% used for config check when the schema module is resolved
|
|
roots() ->
|
|
[
|
|
{?CONF_NS,
|
|
hoconsc:mk(
|
|
hoconsc:union(fun ?MODULE:union_member_selector/1),
|
|
#{}
|
|
)}
|
|
].
|
|
|
|
fields(mongo_single) ->
|
|
common_fields() ++ emqx_connector_mongo:fields(single);
|
|
fields(mongo_rs) ->
|
|
common_fields() ++ emqx_connector_mongo:fields(rs);
|
|
fields(mongo_sharded) ->
|
|
common_fields() ++ emqx_connector_mongo:fields(sharded).
|
|
|
|
desc(mongo_single) ->
|
|
?DESC(single);
|
|
desc(mongo_rs) ->
|
|
?DESC('replica-set');
|
|
desc(mongo_sharded) ->
|
|
?DESC('sharded-cluster');
|
|
desc(_) ->
|
|
undefined.
|
|
|
|
common_fields() ->
|
|
[
|
|
{mechanism, emqx_authn_schema:mechanism(password_based)},
|
|
{backend, emqx_authn_schema:backend(mongodb)},
|
|
{collection, fun collection/1},
|
|
{filter, fun filter/1},
|
|
{password_hash_field, fun password_hash_field/1},
|
|
{salt_field, fun salt_field/1},
|
|
{is_superuser_field, fun is_superuser_field/1},
|
|
{password_hash_algorithm, fun emqx_authn_password_hashing:type_ro/1}
|
|
] ++ emqx_authn_schema:common_fields().
|
|
|
|
collection(type) -> binary();
|
|
collection(desc) -> ?DESC(?FUNCTION_NAME);
|
|
collection(required) -> true;
|
|
collection(_) -> undefined.
|
|
|
|
filter(type) ->
|
|
map();
|
|
filter(desc) ->
|
|
?DESC(?FUNCTION_NAME);
|
|
filter(required) ->
|
|
false;
|
|
filter(default) ->
|
|
#{};
|
|
filter(_) ->
|
|
undefined.
|
|
|
|
password_hash_field(type) -> binary();
|
|
password_hash_field(desc) -> ?DESC(?FUNCTION_NAME);
|
|
password_hash_field(required) -> false;
|
|
password_hash_field(default) -> <<"password_hash">>;
|
|
password_hash_field(_) -> undefined.
|
|
|
|
salt_field(type) -> binary();
|
|
salt_field(desc) -> ?DESC(?FUNCTION_NAME);
|
|
salt_field(required) -> false;
|
|
salt_field(default) -> <<"salt">>;
|
|
salt_field(_) -> undefined.
|
|
|
|
is_superuser_field(type) -> binary();
|
|
is_superuser_field(desc) -> ?DESC(?FUNCTION_NAME);
|
|
is_superuser_field(required) -> false;
|
|
is_superuser_field(default) -> <<"is_superuser">>;
|
|
is_superuser_field(_) -> undefined.
|
|
|
|
%%------------------------------------------------------------------------------
|
|
%% APIs
|
|
%%------------------------------------------------------------------------------
|
|
|
|
refs() ->
|
|
[
|
|
hoconsc:ref(?MODULE, mongo_single),
|
|
hoconsc:ref(?MODULE, mongo_rs),
|
|
hoconsc:ref(?MODULE, mongo_sharded)
|
|
].
|
|
|
|
create(_AuthenticatorID, Config) ->
|
|
create(Config).
|
|
|
|
create(Config0) ->
|
|
ResourceId = emqx_authn_utils:make_resource_id(?MODULE),
|
|
{Config, State} = parse_config(Config0),
|
|
{ok, _Data} = emqx_authn_utils:create_resource(
|
|
ResourceId,
|
|
emqx_connector_mongo,
|
|
Config
|
|
),
|
|
{ok, State#{resource_id => ResourceId}}.
|
|
|
|
update(Config0, #{resource_id := ResourceId} = _State) ->
|
|
{Config, NState} = parse_config(Config0),
|
|
case emqx_authn_utils:update_resource(emqx_connector_mongo, Config, ResourceId) of
|
|
{error, Reason} ->
|
|
error({load_config_error, Reason});
|
|
{ok, _} ->
|
|
{ok, NState#{resource_id => ResourceId}}
|
|
end.
|
|
|
|
destroy(#{resource_id := ResourceId}) ->
|
|
_ = emqx_resource:remove_local(ResourceId),
|
|
ok.
|
|
|
|
authenticate(#{auth_method := _}, _) ->
|
|
ignore;
|
|
authenticate(
|
|
#{password := Password} = Credential,
|
|
#{
|
|
collection := Collection,
|
|
filter_template := FilterTemplate,
|
|
resource_id := ResourceId
|
|
} = State
|
|
) ->
|
|
Filter = emqx_authn_utils:render_deep(FilterTemplate, Credential),
|
|
case emqx_resource:simple_sync_query(ResourceId, {find_one, Collection, Filter, #{}}) of
|
|
{ok, undefined} ->
|
|
ignore;
|
|
{error, Reason} ->
|
|
?TRACE_AUTHN_PROVIDER(error, "mongodb_query_failed", #{
|
|
resource => ResourceId,
|
|
collection => Collection,
|
|
filter => Filter,
|
|
reason => Reason
|
|
}),
|
|
ignore;
|
|
{ok, Doc} ->
|
|
case check_password(Password, Doc, State) of
|
|
ok ->
|
|
{ok, is_superuser(Doc, State)};
|
|
{error, {cannot_find_password_hash_field, PasswordHashField}} ->
|
|
?TRACE_AUTHN_PROVIDER(error, "cannot_find_password_hash_field", #{
|
|
resource => ResourceId,
|
|
collection => Collection,
|
|
filter => Filter,
|
|
document => Doc,
|
|
password_hash_field => PasswordHashField
|
|
}),
|
|
ignore;
|
|
{error, Reason} ->
|
|
{error, Reason}
|
|
end
|
|
end.
|
|
|
|
%%------------------------------------------------------------------------------
|
|
%% Internal functions
|
|
%%------------------------------------------------------------------------------
|
|
|
|
parse_config(#{filter := Filter} = Config) ->
|
|
FilterTemplate = emqx_authn_utils:parse_deep(Filter),
|
|
State = maps:with(
|
|
[
|
|
collection,
|
|
password_hash_field,
|
|
salt_field,
|
|
is_superuser_field,
|
|
password_hash_algorithm,
|
|
salt_position
|
|
],
|
|
Config
|
|
),
|
|
ok = emqx_authn_password_hashing:init(maps:get(password_hash_algorithm, State)),
|
|
{Config, State#{filter_template => FilterTemplate}}.
|
|
|
|
check_password(undefined, _Selected, _State) ->
|
|
{error, bad_username_or_password};
|
|
check_password(
|
|
Password,
|
|
Doc,
|
|
#{
|
|
password_hash_algorithm := Algorithm,
|
|
password_hash_field := PasswordHashField
|
|
} = 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 emqx_authn_password_hashing:check_password(Algorithm, Salt, Hash, Password) of
|
|
true -> ok;
|
|
false -> {error, bad_username_or_password}
|
|
end
|
|
end.
|
|
|
|
is_superuser(Doc, #{is_superuser_field := IsSuperuserField}) ->
|
|
IsSuperuser = maps:get(IsSuperuserField, Doc, false),
|
|
emqx_authn_utils:is_superuser(#{<<"is_superuser">> => IsSuperuser});
|
|
is_superuser(_, _) ->
|
|
emqx_authn_utils:is_superuser(#{<<"is_superuser">> => false}).
|
|
|
|
union_member_selector(all_union_members) ->
|
|
refs();
|
|
union_member_selector({value, Value}) ->
|
|
refs(Value).
|
|
|
|
refs(#{<<"mongo_type">> := <<"single">>}) ->
|
|
[hoconsc:ref(?MODULE, mongo_single)];
|
|
refs(#{<<"mongo_type">> := <<"rs">>}) ->
|
|
[hoconsc:ref(?MODULE, mongo_rs)];
|
|
refs(#{<<"mongo_type">> := <<"sharded">>}) ->
|
|
[hoconsc:ref(?MODULE, mongo_sharded)];
|
|
refs(_) ->
|
|
throw(#{
|
|
field_name => mongo_type,
|
|
expected => "single | rs | sharded"
|
|
}).
|