emqx/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl

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"
}).