emqx/src/emqx_access_control.erl

133 lines
5.0 KiB
Erlang

%%--------------------------------------------------------------------
%% Copyright (c) 2017-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_access_control).
-include("emqx.hrl").
-export([authenticate/1]).
-export([ check_acl/3
]).
-type(result() :: #{auth_result := emqx_types:auth_result(),
anonymous := boolean()
}).
%%--------------------------------------------------------------------
%% APIs
%%--------------------------------------------------------------------
-spec(authenticate(emqx_types:clientinfo()) -> {ok, result()} | {error, term()}).
authenticate(ClientInfo = #{zone := Zone}) ->
ok = emqx_metrics:inc('client.authenticate'),
Username = maps:get(username, ClientInfo, undefined),
{MaybeStop, AuthResult} = default_auth_result(Username, Zone),
case MaybeStop of
stop ->
return_auth_result(AuthResult);
continue ->
return_auth_result(emqx_hooks:run_fold('client.authenticate', [ClientInfo], AuthResult))
end.
%% @doc Check ACL
-spec(check_acl(emqx_types:clientinfo(), emqx_types:pubsub(), emqx_types:topic())
-> allow | deny).
check_acl(ClientInfo, PubSub, Topic) ->
Result = case emqx_acl_cache:is_enabled() of
true -> check_acl_cache(ClientInfo, PubSub, Topic);
false -> do_check_acl(ClientInfo, PubSub, Topic)
end,
inc_acl_metrics(Result),
Result.
%%--------------------------------------------------------------------
%% Internal functions
%%--------------------------------------------------------------------
%% ACL
check_acl_cache(ClientInfo, PubSub, Topic) ->
case emqx_acl_cache:get_acl_cache(PubSub, Topic) of
not_found ->
AclResult = do_check_acl(ClientInfo, PubSub, Topic),
emqx_acl_cache:put_acl_cache(PubSub, Topic, AclResult),
AclResult;
AclResult ->
inc_acl_metrics(cache_hit),
emqx:run_hook('client.check_acl_complete', [ClientInfo, PubSub, Topic, AclResult, true]),
AclResult
end.
do_check_acl(ClientInfo = #{zone := Zone}, PubSub, Topic) ->
Default = emqx_zone:get_env(Zone, acl_nomatch, deny),
ok = emqx_metrics:inc('client.check_acl'),
Result = case emqx_hooks:run_fold('client.check_acl', [ClientInfo, PubSub, Topic], Default) of
allow -> allow;
_Other -> deny
end,
emqx:run_hook('client.check_acl_complete', [ClientInfo, PubSub, Topic, Result, false]),
Result.
-compile({inline, [inc_acl_metrics/1]}).
inc_acl_metrics(allow) ->
emqx_metrics:inc('client.acl.allow');
inc_acl_metrics(deny) ->
emqx_metrics:inc('client.acl.deny');
inc_acl_metrics(cache_hit) ->
emqx_metrics:inc('client.acl.cache_hit').
%% Auth
default_auth_result(Username, Zone) ->
IsAnonymous = (Username =:= undefined orelse Username =:= <<>>),
AllowAnonymous = emqx_zone:get_env(Zone, allow_anonymous, false),
Bypass = emqx_zone:get_env(Zone, bypass_auth_plugins, false),
%% the `anonymous` field in auth result does not mean the client is
%% connected without username, but if the auth result is based on
%% allowing anonymous access.
IsResultBasedOnAllowAnonymous =
case AllowAnonymous of
true -> true;
_ -> false
end,
Result = case AllowAnonymous of
true -> #{auth_result => success, anonymous => IsResultBasedOnAllowAnonymous};
_ -> #{auth_result => not_authorized, anonymous => IsResultBasedOnAllowAnonymous}
end,
case {IsAnonymous, AllowAnonymous} of
{true, false_quick_deny} ->
{stop, Result};
_ when Bypass ->
{stop, Result};
_ ->
{continue, Result}
end.
-compile({inline, [return_auth_result/1]}).
return_auth_result(AuthResult = #{auth_result := success}) ->
inc_auth_success_metrics(AuthResult),
{ok, AuthResult};
return_auth_result(AuthResult) ->
emqx_metrics:inc('client.auth.failure'),
{error, maps:get(auth_result, AuthResult, unknown_error)}.
-compile({inline, [inc_auth_success_metrics/1]}).
inc_auth_success_metrics(AuthResult) ->
is_anonymous(AuthResult) andalso
emqx_metrics:inc('client.auth.success.anonymous'),
emqx_metrics:inc('client.auth.success').
is_anonymous(#{anonymous := true}) -> true;
is_anonymous(_AuthResult) -> false.