emqx/apps/emqx_authz/src/emqx_authz.erl

265 lines
9.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_authz).
-include("emqx_authz.hrl").
-include_lib("emqx/include/logger.hrl").
-logger_header("[AuthZ]").
-export([ register_metrics/0
, init/0
, compile/1
, lookup/0
, update/1
, authorize/5
, match/4
]).
-spec(register_metrics() -> ok).
register_metrics() ->
lists:foreach(fun emqx_metrics:ensure/1, ?AUTHZ_METRICS).
init() ->
ok = register_metrics(),
Rules = emqx_config:get([emqx_authz, rules], []),
NRules = [compile(Rule) || Rule <- Rules],
ok = emqx_hooks:add('client.authorize', {?MODULE, authorize, [NRules]}, -1).
lookup() ->
emqx_config:get([emqx_authz, rules], []).
update(Rules) ->
emqx_config:put([emqx_authz], #{rules => Rules}),
NRules = [compile(Rule) || Rule <- Rules],
Action = find_action_in_hooks(),
ok = emqx_hooks:del('client.authorize', Action),
ok = emqx_hooks:add('client.authorize', {?MODULE, authorize, [NRules]}, -1),
ok = emqx_acl_cache:empty_acl_cache().
%%--------------------------------------------------------------------
%% Internal functions
%%--------------------------------------------------------------------
find_action_in_hooks() ->
Callbacks = emqx_hooks:lookup('client.authorize'),
[Action] = [Action || {callback,{?MODULE, authorize, _} = Action, _, _} <- Callbacks ],
Action.
create_resource(#{type := DB,
config := Config
} = Rule) ->
ResourceID = iolist_to_binary([io_lib:format("~s_~s",[?APP, DB]), "_", integer_to_list(erlang:system_time())]),
NConfig = case DB of
redis -> #{config => Config };
mongo -> #{config => Config };
_ -> Config
end,
case emqx_resource:check_and_create(
ResourceID,
list_to_existing_atom(io_lib:format("~s_~s",[emqx_connector, DB])),
NConfig)
of
{ok, _} ->
Rule#{resource_id => ResourceID};
{error, already_created} ->
Rule#{resource_id => ResourceID};
{error, Reason} ->
error({load_config_error, Reason})
end.
-spec(compile(rule()) -> rule()).
compile(#{topics := Topics,
action := Action,
permission := Permission,
principal := Principal
} = Rule) when ?ALLOW_DENY(Permission), ?PUBSUB(Action), is_list(Topics) ->
NTopics = [compile_topic(Topic) || Topic <- Topics],
Rule#{principal => compile_principal(Principal),
topics => NTopics
};
compile(#{principal := Principal,
type := DB
} = Rule) when DB =:= redis;
DB =:= mongo ->
NRule = create_resource(Rule),
NRule#{principal => compile_principal(Principal)};
compile(#{principal := Principal,
type := DB,
sql := SQL
} = Rule) when DB =:= mysql;
DB =:= pgsql ->
Mod = list_to_existing_atom(io_lib:format("~s_~s",[?APP, DB])),
NRule = create_resource(Rule),
NRule#{principal => compile_principal(Principal),
sql => Mod:parse_query(SQL)
}.
compile_principal(all) -> all;
compile_principal(#{username := Username}) ->
{ok, MP} = re:compile(bin(Username)),
#{username => MP};
compile_principal(#{clientid := Clientid}) ->
{ok, MP} = re:compile(bin(Clientid)),
#{clientid => MP};
compile_principal(#{ipaddress := IpAddress}) ->
#{ipaddress => esockd_cidr:parse(b2l(IpAddress), true)};
compile_principal(#{'and' := Principals}) when is_list(Principals) ->
#{'and' => [compile_principal(Principal) || Principal <- Principals]};
compile_principal(#{'or' := Principals}) when is_list(Principals) ->
#{'or' => [compile_principal(Principal) || Principal <- Principals]}.
compile_topic(<<"eq ", Topic/binary>>) ->
compile_topic(#{'eq' => Topic});
compile_topic(#{'eq' := Topic}) ->
#{'eq' => emqx_topic:words(bin(Topic))};
compile_topic(Topic) when is_binary(Topic)->
Words = emqx_topic:words(bin(Topic)),
case pattern(Words) of
true -> #{pattern => Words};
false -> Words
end.
pattern(Words) ->
lists:member(<<"%u">>, Words) orelse lists:member(<<"%c">>, Words).
bin(A) when is_atom(A) -> atom_to_binary(A, utf8);
bin(B) when is_binary(B) -> B;
bin(L) when is_list(L) -> list_to_binary(L);
bin(X) -> X.
b2l(B) when is_list(B) -> B;
b2l(B) when is_binary(B) -> binary_to_list(B).
%%--------------------------------------------------------------------
%% AuthZ callbacks
%%--------------------------------------------------------------------
%% @doc Check AuthZ
-spec(authorize(emqx_types:clientinfo(), emqx_types:all(), emqx_topic:topic(), emqx_permission_rule:acl_result(), rules())
-> {stop, allow} | {ok, deny}).
authorize(#{username := Username,
peerhost := IpAddress
} = Client, PubSub, Topic, _DefaultResult, Rules) ->
case do_authorize(Client, PubSub, Topic, Rules) of
{matched, allow} ->
?LOG(info, "Client succeeded authorization: Username: ~p, IP: ~p, Topic: ~p, Permission: allow", [Username, IpAddress, Topic]),
emqx_metrics:inc(?AUTHZ_METRICS(allow)),
{stop, allow};
{matched, deny} ->
?LOG(info, "Client failed authorization: Username: ~p, IP: ~p, Topic: ~p, Permission: deny", [Username, IpAddress, Topic]),
emqx_metrics:inc(?AUTHZ_METRICS(deny)),
{stop, deny};
nomatch ->
?LOG(info, "Client failed authorization: Username: ~p, IP: ~p, Topic: ~p, Reasion: ~p", [Username, IpAddress, Topic, "no-match rule"]),
{stop, deny}
end.
do_authorize(Client, PubSub, Topic,
[Connector = #{principal := Principal,
type := DB} | Tail] ) ->
case match_principal(Client, Principal) of
true ->
Mod = list_to_existing_atom(io_lib:format("~s_~s",[emqx_authz, DB])),
case Mod:authorize(Client, PubSub, Topic, Connector) of
nomatch -> do_authorize(Client, PubSub, Topic, Tail);
Matched -> Matched
end;
false -> do_authorize(Client, PubSub, Topic, Tail)
end;
do_authorize(Client, PubSub, Topic,
[#{permission := Permission} = Rule | Tail]) ->
case match(Client, PubSub, Topic, Rule) of
true -> {matched, Permission};
false -> do_authorize(Client, PubSub, Topic, Tail)
end;
do_authorize(_Client, _PubSub, _Topic, []) -> nomatch.
match(Client, PubSub, Topic,
#{principal := Principal,
topics := TopicFilters,
action := Action
}) ->
match_action(PubSub, Action) andalso
match_principal(Client, Principal) andalso
match_topics(Client, Topic, TopicFilters).
match_action(publish, publish) -> true;
match_action(subscribe, subscribe) -> true;
match_action(_, all) -> true;
match_action(_, _) -> false.
match_principal(_, all) -> true;
match_principal(#{username := undefined}, #{username := _MP}) ->
false;
match_principal(#{username := Username}, #{username := MP}) ->
case re:run(Username, MP) of
{match, _} -> true;
_ -> false
end;
match_principal(#{clientid := Clientid}, #{clientid := MP}) ->
case re:run(Clientid, MP) of
{match, _} -> true;
_ -> false
end;
match_principal(#{peerhost := undefined}, #{ipaddress := _CIDR}) ->
false;
match_principal(#{peerhost := IpAddress}, #{ipaddress := CIDR}) ->
esockd_cidr:match(IpAddress, CIDR);
match_principal(ClientInfo, #{'and' := Principals}) when is_list(Principals) ->
lists:foldl(fun(Principal, Permission) ->
match_principal(ClientInfo, Principal) andalso Permission
end, true, Principals);
match_principal(ClientInfo, #{'or' := Principals}) when is_list(Principals) ->
lists:foldl(fun(Principal, Permission) ->
match_principal(ClientInfo, Principal) orelse Permission
end, false, Principals);
match_principal(_, _) -> false.
match_topics(_ClientInfo, _Topic, []) ->
false;
match_topics(ClientInfo, Topic, [#{pattern := PatternFilter}|Filters]) ->
TopicFilter = feed_var(ClientInfo, PatternFilter),
match_topic(emqx_topic:words(Topic), TopicFilter)
orelse match_topics(ClientInfo, Topic, Filters);
match_topics(ClientInfo, Topic, [TopicFilter|Filters]) ->
match_topic(emqx_topic:words(Topic), TopicFilter)
orelse match_topics(ClientInfo, Topic, Filters).
match_topic(Topic, #{'eq' := TopicFilter}) ->
Topic == TopicFilter;
match_topic(Topic, TopicFilter) ->
emqx_topic:match(Topic, TopicFilter).
feed_var(ClientInfo, Pattern) ->
feed_var(ClientInfo, Pattern, []).
feed_var(_ClientInfo, [], Acc) ->
lists:reverse(Acc);
feed_var(ClientInfo = #{clientid := undefined}, [<<"%c">>|Words], Acc) ->
feed_var(ClientInfo, Words, [<<"%c">>|Acc]);
feed_var(ClientInfo = #{clientid := ClientId}, [<<"%c">>|Words], Acc) ->
feed_var(ClientInfo, Words, [ClientId |Acc]);
feed_var(ClientInfo = #{username := undefined}, [<<"%u">>|Words], Acc) ->
feed_var(ClientInfo, Words, [<<"%u">>|Acc]);
feed_var(ClientInfo = #{username := Username}, [<<"%u">>|Words], Acc) ->
feed_var(ClientInfo, Words, [Username|Acc]);
feed_var(ClientInfo, [W|Words], Acc) ->
feed_var(ClientInfo, Words, [W|Acc]).