emqx/apps/emqx_auth_redis/src/emqx_authn_redis.erl

169 lines
5.4 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_redis).
-include_lib("emqx_auth/include/emqx_authn.hrl").
-include_lib("emqx/include/logger.hrl").
-behaviour(emqx_authn_provider).
-export([
create/2,
update/2,
authenticate/2,
destroy/1
]).
%%------------------------------------------------------------------------------
%% APIs
%%------------------------------------------------------------------------------
create(_AuthenticatorID, Config) ->
create(Config).
create(Config0) ->
ResourceId = emqx_authn_utils:make_resource_id(?MODULE),
case parse_config(Config0) of
{error, _} = Res ->
Res;
{Config, State} ->
{ok, _Data} = emqx_authn_utils:create_resource(
ResourceId,
emqx_redis,
Config
),
{ok, State#{resource_id => ResourceId}}
end.
update(Config0, #{resource_id := ResourceId} = _State) ->
{Config, NState} = parse_config(Config0),
case emqx_authn_utils:update_resource(emqx_redis, 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 := undefined}, _) ->
{error, bad_username_or_password};
authenticate(
#{password := Password} = Credential,
#{
cmd := {CommandName, KeyTemplate, Fields},
resource_id := ResourceId,
password_hash_algorithm := Algorithm
}
) ->
NKey = emqx_authn_utils:render_str(KeyTemplate, Credential),
Command = [CommandName, NKey | Fields],
case emqx_resource:simple_sync_query(ResourceId, {cmd, Command}) of
{ok, []} ->
ignore;
{ok, Values} ->
case merge(Fields, Values) of
Selected when Selected =/= #{} ->
case
emqx_authn_utils:check_password_from_selected_map(
Algorithm, Selected, Password
)
of
ok ->
{ok, emqx_authn_utils:is_superuser(Selected)};
{error, _Reason} = Error ->
Error
end;
_ ->
?TRACE_AUTHN_PROVIDER(info, "redis_query_not_matched", #{
resource => ResourceId,
cmd => Command,
keys => NKey,
fields => Fields
}),
ignore
end;
{error, Reason} ->
?TRACE_AUTHN_PROVIDER(error, "redis_query_failed", #{
resource => ResourceId,
cmd => Command,
keys => NKey,
fields => Fields,
reason => Reason
}),
ignore
end.
%%------------------------------------------------------------------------------
%% Internal functions
%%------------------------------------------------------------------------------
parse_config(
#{
cmd := CmdStr,
password_hash_algorithm := Algorithm
} = Config
) ->
case parse_cmd(CmdStr) of
{ok, Cmd} ->
ok = emqx_authn_password_hashing:init(Algorithm),
ok = emqx_authn_utils:ensure_apps_started(Algorithm),
State = maps:with([password_hash_algorithm, salt_position], Config),
{Config, State#{cmd => Cmd}};
{error, _} = Error ->
Error
end.
parse_cmd(CmdStr) ->
case emqx_redis_command:split(CmdStr) of
{ok, Cmd} ->
case validate_cmd(Cmd) of
ok ->
[CommandName, Key | Fields] = Cmd,
{ok, {CommandName, emqx_authn_utils:parse_str(Key), Fields}};
{error, _} = Error ->
Error
end;
{error, _} = Error ->
Error
end.
validate_cmd(Cmd) ->
emqx_auth_redis_validations:validate_command(
[
not_empty,
{command_name, [<<"hget">>, <<"hmget">>]},
{allowed_fields, [<<"password_hash">>, <<"password">>, <<"salt">>, <<"is_superuser">>]},
{required_field_one_of, [<<"password_hash">>, <<"password">>]}
],
Cmd
).
merge(Fields, Value) when not is_list(Value) ->
merge(Fields, [Value]);
merge(Fields, Values) ->
maps:from_list(
[
{K, V}
|| {K, V} <- lists:zip(Fields, Values), V =/= undefined
]
).