265 lines
9.9 KiB
Erlang
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]).
|
|
|