feat: add authz (#4852)
* feat(authorization): add authorization api * feat(authorization): add check function * feat(authorization): use hocon config file * feat(authz): add mysql connector * feat(authz): support pgsql * feat(connector): support redis * chore(authz): use "publish/subscribe/all" instead of "pub/sub/pubsub"
This commit is contained in:
parent
871f23f047
commit
39e4d348f6
|
@ -0,0 +1,19 @@
|
|||
.rebar3
|
||||
_*
|
||||
.eunit
|
||||
*.o
|
||||
*.beam
|
||||
*.plt
|
||||
*.swp
|
||||
*.swo
|
||||
.erlang.cookie
|
||||
ebin
|
||||
log
|
||||
erl_crash.dump
|
||||
.rebar
|
||||
logs
|
||||
_build
|
||||
.idea
|
||||
*.iml
|
||||
rebar3.crashdump
|
||||
*~
|
|
@ -0,0 +1,130 @@
|
|||
# emqx_authz
|
||||
|
||||
## Configure
|
||||
|
||||
File: etc/pulgins/authz.conf
|
||||
|
||||
```json
|
||||
authz:{
|
||||
rules: [
|
||||
{
|
||||
type: mysql
|
||||
config: {
|
||||
server: "127.0.0.1:3306"
|
||||
database: mqtt
|
||||
pool_size: 1
|
||||
username: root
|
||||
password: public
|
||||
auto_reconnect: true
|
||||
ssl: false
|
||||
}
|
||||
sql: "select ipaddress, username, clientid, action, permission, topic from mqtt_acl where ipaddr = '%a' or username = '%u' or clientid = '%c'"
|
||||
},
|
||||
{
|
||||
type: pgsql
|
||||
config: {
|
||||
server: "127.0.0.1:5432"
|
||||
database: mqtt
|
||||
pool_size: 1
|
||||
username: root
|
||||
password: public
|
||||
auto_reconnect: true
|
||||
ssl: false
|
||||
}
|
||||
sql: "select ipaddress, username, clientid, action, permission, topic from mqtt_acl where ipaddr = '%a' or username = '%u' or username = '$all' or clientid = '%c'"
|
||||
},
|
||||
{
|
||||
type: redis
|
||||
config: {
|
||||
servers: "127.0.0.1:6379"
|
||||
database: 0
|
||||
pool_size: 1
|
||||
password: public
|
||||
auto_reconnect: true
|
||||
ssl: false
|
||||
}
|
||||
cmd: "HGETALL mqtt_acl:%u"
|
||||
},
|
||||
{
|
||||
principal: {username: "^admin?"}
|
||||
permission: allow
|
||||
action: subscribe
|
||||
topics: ["$SYS/#"]
|
||||
},
|
||||
{
|
||||
permission: deny
|
||||
action: subscribe
|
||||
topics: ["$SYS/#"]
|
||||
},
|
||||
{
|
||||
permission: allow
|
||||
action: all
|
||||
topics: ["#"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Database Management
|
||||
|
||||
#### Mysql
|
||||
|
||||
Create Example Table
|
||||
|
||||
```sql
|
||||
CREATE TABLE `mqtt_acl` (
|
||||
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`ipaddress` VARCHAR(60) NOT NULL DEFAULT '',
|
||||
`username` VARCHAR(100) NOT NULL DEFAULT '',
|
||||
`clientid` VARCHAR(100) NOT NULL DEFAULT '',
|
||||
`action` ENUM('publish', 'subscribe', 'all') NOT NULL,
|
||||
`permission` ENUM('allow', 'deny') NOT NULL,
|
||||
`topic` VARCHAR(100) NOT NULL DEFAULT '',
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
```
|
||||
|
||||
Sample data in the default configuration:
|
||||
|
||||
```sql
|
||||
-- Only 127.0.0.1 users can subscribe to system topics
|
||||
INSERT INTO mqtt_acl (ipaddress, username, clientid, action, permission, topic) VALUES ('127.0.0.1', '', '', 'subscribe', 'allow', '$SYS/#');
|
||||
```
|
||||
|
||||
#### Pgsql
|
||||
|
||||
Create Example Table
|
||||
|
||||
```sql
|
||||
CREATE TYPE ACTION AS ENUM('publish','subscribe','all');
|
||||
CREATE TYPE PERMISSION AS ENUM('allow','deny');
|
||||
|
||||
CREATE TABLE mqtt_acl (
|
||||
id SERIAL PRIMARY KEY,
|
||||
ipaddress CHARACTER VARYING(60) NOT NULL DEFAULT '',
|
||||
username CHARACTER VARYING(100) NOT NULL DEFAULT '',
|
||||
clientid CHARACTER VARYING(100) NOT NULL DEFAULT '',
|
||||
action ACTION,
|
||||
permission PERMISSION,
|
||||
topic CHARACTER VARYING(100) NOT NULL
|
||||
);
|
||||
|
||||
```
|
||||
|
||||
Sample data in the default configuration:
|
||||
|
||||
```sql
|
||||
-- Only 127.0.0.1 users can subscribe to system topics
|
||||
INSERT INTO mqtt_acl (ipaddress, username, clientid, action, permission, topic) VALUES ('127.0.0.1', '', '', 'subscribe', 'allow', '$SYS/#');
|
||||
```
|
||||
|
||||
#### Redis
|
||||
|
||||
Sample data in the default configuration:
|
||||
|
||||
```
|
||||
HSET mqtt_acl:emqx '$SYS/#' subscribe
|
||||
```
|
||||
|
||||
A rule of Redis ACL defines `publish`, `subscribe`, or `all `information. All lists in the rule are **allow** lists.
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
authz:{
|
||||
rules: [
|
||||
# {
|
||||
# type: mysql
|
||||
# config: {
|
||||
# server: "127.0.0.1:3306"
|
||||
# database: mqtt
|
||||
# pool_size: 1
|
||||
# username: root
|
||||
# password: public
|
||||
# auto_reconnect: true
|
||||
# ssl: false
|
||||
# }
|
||||
# sql: "select ipaddress, username, clientid, action, permission, topic from mqtt_acl where ipaddr = '%a' or username = '%u' or clientid = '%c'"
|
||||
# },
|
||||
# {
|
||||
# type: pgsql
|
||||
# config: {
|
||||
# server: "127.0.0.1:5432"
|
||||
# database: mqtt
|
||||
# pool_size: 1
|
||||
# username: root
|
||||
# password: public
|
||||
# auto_reconnect: true
|
||||
# ssl: false
|
||||
# }
|
||||
# sql: "select ipaddress, username, clientid, action, permission, topic from mqtt_acl where ipaddr = '%a' or username = '%u' or username = '$all' or clientid = '%c'"
|
||||
# },
|
||||
# {
|
||||
# type: redis
|
||||
# config: {
|
||||
# servers: "127.0.0.1:6379"
|
||||
# database: 0
|
||||
# pool_size: 1
|
||||
# password: public
|
||||
# auto_reconnect: true
|
||||
# ssl: false
|
||||
# }
|
||||
# cmd: "HGETALL mqtt_acl:%u"
|
||||
# },
|
||||
{
|
||||
permission: allow
|
||||
action: all
|
||||
topics: ["#"]
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
-type(rule() :: #{binary() => any()}).
|
||||
-type(rules() :: [rule()]).
|
||||
|
||||
-define(APP, emqx_authz).
|
||||
|
||||
-define(ALLOW_DENY(A), ((A =:= allow) orelse (A =:= deny))).
|
||||
-define(PUBSUB(A), ((A =:= subscribe) orelse (A =:= publish) orelse (A =:= all))).
|
||||
|
||||
-record(acl_metrics, {
|
||||
allow = 'client.acl.allow',
|
||||
deny = 'client.acl.deny',
|
||||
ignore = 'client.acl.ignore'
|
||||
}).
|
||||
|
||||
-define(METRICS(Type), tl(tuple_to_list(#Type{}))).
|
||||
-define(METRICS(Type, K), #Type{}#Type.K).
|
||||
|
||||
-define(ACL_METRICS, ?METRICS(acl_metrics)).
|
||||
-define(ACL_METRICS(K), ?METRICS(acl_metrics, K)).
|
|
@ -0,0 +1,7 @@
|
|||
{erl_opts, [debug_info, nowarn_unused_import]}.
|
||||
{deps, []}.
|
||||
|
||||
{shell, [
|
||||
% {config, "config/sys.config"},
|
||||
{apps, [emqx_authz]}
|
||||
]}.
|
|
@ -0,0 +1,16 @@
|
|||
{application, emqx_authz,
|
||||
[{description, "An OTP application"},
|
||||
{vsn, "0.1.0"},
|
||||
{registered, []},
|
||||
{mod, {emqx_authz_app, []}},
|
||||
{applications,
|
||||
[kernel,
|
||||
stdlib,
|
||||
emqx_connector
|
||||
]},
|
||||
{env,[]},
|
||||
{modules, []},
|
||||
|
||||
{licenses, ["Apache 2.0"]},
|
||||
{links, []}
|
||||
]}.
|
|
@ -0,0 +1,261 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% 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
|
||||
, check_authz/5
|
||||
, match/4
|
||||
]).
|
||||
|
||||
-spec(register_metrics() -> ok).
|
||||
register_metrics() ->
|
||||
lists:foreach(fun emqx_metrics:ensure/1, ?ACL_METRICS).
|
||||
|
||||
init() ->
|
||||
ok = register_metrics(),
|
||||
Conf = filename:join(emqx:get_env(plugins_etc_dir), 'authz.conf'),
|
||||
{ok, RawConf} = hocon:load(Conf),
|
||||
#{<<"authz">> := #{<<"rules">> := Rules}} = hocon_schema:check_plain(emqx_authz_schema, RawConf),
|
||||
ok = application:set_env(?APP, rules, Rules),
|
||||
NRules = [compile(Rule) || Rule <- Rules],
|
||||
ok = emqx_hooks:add('client.check_acl', {?MODULE, check_authz, [NRules]}, -1).
|
||||
|
||||
lookup() ->
|
||||
application:get_env(?APP, rules, []).
|
||||
|
||||
update(Rules) ->
|
||||
ok = application:set_env(?APP, rules, Rules),
|
||||
NRules = [compile(Rule) || Rule <- Rules],
|
||||
Action = find_action_in_hooks(),
|
||||
ok = emqx_hooks:del('client.check_acl', Action),
|
||||
ok = emqx_hooks:add('client.check_acl', {?MODULE, check_authz, [NRules]}, -1),
|
||||
ok = emqx_acl_cache:empty_acl_cache().
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Internal functions
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
find_action_in_hooks() ->
|
||||
Callbacks = emqx_hooks:lookup('client.check_acl'),
|
||||
[Action] = [Action || {callback,{?MODULE, check_authz, _} = 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())]),
|
||||
case emqx_resource:check_and_create(
|
||||
ResourceID,
|
||||
list_to_existing_atom(io_lib:format("~s_~s",[emqx_connector, DB])),
|
||||
Config)
|
||||
of
|
||||
{ok, _} ->
|
||||
Rule#{<<"resource_id">> => ResourceID};
|
||||
{error, already_created} ->
|
||||
Rule#{<<"resource_id">> => ResourceID};
|
||||
{error, Reason} ->
|
||||
error({load_sql_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">> := redis
|
||||
} = Rule) ->
|
||||
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).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% ACL callbacks
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
%% @doc Check ACL
|
||||
-spec(check_authz(emqx_types:clientinfo(), emqx_types:all(), emqx_topic:topic(), emqx_permission_rule:acl_result(), rules())
|
||||
-> {ok, allow} | {ok, deny} | deny).
|
||||
check_authz(#{username := Username,
|
||||
peerhost := IpAddress
|
||||
} = Client, PubSub, Topic, DefaultResult, Rules) ->
|
||||
case do_check_authz(Client, PubSub, Topic, Rules) of
|
||||
{matched, allow} ->
|
||||
?LOG(info, "Client succeeded authorizationa: Username: ~p, IP: ~p, Topic: ~p, Permission: allow", [Username, IpAddress, Topic]),
|
||||
emqx_metrics:inc(?ACL_METRICS(allow)),
|
||||
{stop, allow};
|
||||
{matched, deny} ->
|
||||
?LOG(info, "Client failed authorizationa: Username: ~p, IP: ~p, Topic: ~p, Permission: deny", [Username, IpAddress, Topic]),
|
||||
emqx_metrics:inc(?ACL_METRICS(deny)),
|
||||
{stop, deny};
|
||||
nomatch ->
|
||||
?LOG(info, "Client failed authorizationa: Username: ~p, IP: ~p, Topic: ~p, Reasion: ~p", [Username, IpAddress, Topic, "no-match rule"]),
|
||||
DefaultResult
|
||||
end.
|
||||
|
||||
do_check_authz(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:check_authz(Client, PubSub, Topic, Connector) of
|
||||
nomatch -> do_check_authz(Client, PubSub, Topic, Tail);
|
||||
Matched -> Matched
|
||||
end;
|
||||
false -> do_check_authz(Client, PubSub, Topic, Tail)
|
||||
end;
|
||||
do_check_authz(Client, PubSub, Topic,
|
||||
[#{<<"permission">> := Permission} = Rule | Tail]) ->
|
||||
case match(Client, PubSub, Topic, Rule) of
|
||||
true -> {matched, Permission};
|
||||
false -> do_check_authz(Client, PubSub, Topic, Tail)
|
||||
end;
|
||||
do_check_authz(_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]).
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% 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_api).
|
||||
|
||||
-include("emqx_authz.hrl").
|
||||
|
||||
-rest_api(#{name => lookup_authz,
|
||||
method => 'GET',
|
||||
path => "/authz",
|
||||
func => lookup_authz,
|
||||
descr => "Lookup Authorization"
|
||||
}).
|
||||
|
||||
-rest_api(#{name => update_authz,
|
||||
method => 'PUT',
|
||||
path => "/authz",
|
||||
func => update_authz,
|
||||
descr => "Rewrite authz list"
|
||||
}).
|
||||
|
||||
-rest_api(#{name => append_authz,
|
||||
method => 'POST',
|
||||
path => "/authz/append",
|
||||
func => append_authz,
|
||||
descr => "Add a new rule at the end of the authz list"
|
||||
}).
|
||||
|
||||
-rest_api(#{name => push_authz,
|
||||
method => 'POST',
|
||||
path => "/authz/push",
|
||||
func => push_authz,
|
||||
descr => "Add a new rule at the start of the authz list"
|
||||
}).
|
||||
|
||||
-export([ lookup_authz/2
|
||||
, update_authz/2
|
||||
, append_authz/2
|
||||
, push_authz/2
|
||||
]).
|
||||
|
||||
lookup_authz(_Bindings, _Params) ->
|
||||
minirest:return({ok, emqx_authz:lookup()}).
|
||||
|
||||
update_authz(_Bindings, Params) ->
|
||||
Rules = get_rules(Params),
|
||||
minirest:return(emqx_authz:update(Rules)).
|
||||
|
||||
append_authz(_Bindings, Params) ->
|
||||
Rules = get_rules(Params),
|
||||
NRules = lists:append(emqx_authz:lookup(), Rules),
|
||||
minirest:return(emqx_authz:update(NRules)).
|
||||
|
||||
push_authz(_Bindings, Params) ->
|
||||
Rules = get_rules(Params),
|
||||
NRules = lists:append(Rules, emqx_authz:lookup()),
|
||||
minirest:return(emqx_authz:update(NRules)).
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
%% Interval Funcs
|
||||
%%------------------------------------------------------------------------------
|
||||
|
||||
get_rules(Params) ->
|
||||
% #{<<"authz">> := #{<<"rules">> := Rules}} = hocon_schema:check_plain(emqx_authz_schema, #{<<"authz">> => Params}),
|
||||
{ok, Conf} = hocon:binary(jsx:encode(#{<<"authz">> => Params}), #{format => richmap}),
|
||||
CheckConf = hocon_schema:check(emqx_authz_schema, Conf),
|
||||
#{<<"authz">> := #{<<"rules">> := Rules}} = hocon_schema:richmap_to_map(CheckConf),
|
||||
Rules.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% EUnits
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-ifdef(TEST).
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
|
||||
|
||||
-endif.
|
|
@ -0,0 +1,20 @@
|
|||
%%%-------------------------------------------------------------------
|
||||
%% @doc emqx_authz public API
|
||||
%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
|
||||
-module(emqx_authz_app).
|
||||
|
||||
-behaviour(application).
|
||||
|
||||
-export([start/2, stop/1]).
|
||||
|
||||
start(_StartType, _StartArgs) ->
|
||||
{ok, Sup} = emqx_authz_sup:start_link(),
|
||||
ok = emqx_authz:init(),
|
||||
{ok, Sup}.
|
||||
|
||||
stop(_State) ->
|
||||
ok.
|
||||
|
||||
%% internal functions
|
|
@ -0,0 +1,141 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% 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_mysql).
|
||||
|
||||
-include("emqx_authz.hrl").
|
||||
-include_lib("emqx/include/emqx.hrl").
|
||||
-include_lib("emqx/include/logger.hrl").
|
||||
|
||||
%% ACL Callbacks
|
||||
-export([ description/0
|
||||
, parse_query/1
|
||||
, check_authz/4
|
||||
]).
|
||||
|
||||
-ifdef(TEST).
|
||||
-compile(export_all).
|
||||
-compile(nowarn_export_all).
|
||||
-endif.
|
||||
|
||||
description() ->
|
||||
"AuthZ with Mysql".
|
||||
|
||||
parse_query(undefined) ->
|
||||
undefined;
|
||||
parse_query(Sql) ->
|
||||
case re:run(Sql, "'%[ucCad]'", [global, {capture, all, list}]) of
|
||||
{match, Variables} ->
|
||||
Params = [Var || [Var] <- Variables],
|
||||
{re:replace(Sql, "'%[ucCad]'", "?", [global, {return, list}]), Params};
|
||||
nomatch ->
|
||||
{Sql, []}
|
||||
end.
|
||||
|
||||
check_authz(Client, PubSub, Topic,
|
||||
#{<<"resource_id">> := ResourceID,
|
||||
<<"sql">> := {SQL, Params}
|
||||
}) ->
|
||||
case emqx_resource:query(ResourceID, {sql, SQL, replvar(Params, Client)}) of
|
||||
{ok, _Columns, []} -> nomatch;
|
||||
{ok, Columns, Rows} ->
|
||||
do_check_authz(Client, PubSub, Topic, Columns, Rows);
|
||||
{error, Reason} ->
|
||||
?LOG(error, "[AuthZ] Query mysql error: ~p~n", [Reason]),
|
||||
nomatch
|
||||
end.
|
||||
|
||||
do_check_authz(_Client, _PubSub, _Topic, _Columns, []) ->
|
||||
nomatch;
|
||||
do_check_authz(Client, PubSub, Topic, Columns, [Row | Tail]) ->
|
||||
case match(Client, PubSub, Topic, format_result(Columns, Row)) of
|
||||
{matched, Permission} -> {matched, Permission};
|
||||
nomatch -> do_check_authz(Client, PubSub, Topic, Columns, Tail)
|
||||
end.
|
||||
|
||||
format_result(Columns, Row) ->
|
||||
L = [ begin
|
||||
K = lists:nth(I, Columns),
|
||||
V = lists:nth(I, Row),
|
||||
{K, V}
|
||||
end || I <- lists:seq(1, length(Columns)) ],
|
||||
maps:from_list(L).
|
||||
|
||||
match(Client, PubSub, Topic,
|
||||
#{<<"permission">> := Permission,
|
||||
<<"action">> := Action,
|
||||
<<"clientid">> := ClientId,
|
||||
<<"username">> := Username,
|
||||
<<"ipaddress">> := IpAddress,
|
||||
<<"topic">> := TopicFilter
|
||||
}) ->
|
||||
Rule = #{<<"principal">> => principal(IpAddress, Username, ClientId),
|
||||
<<"topics">> => [TopicFilter],
|
||||
<<"action">> => Action,
|
||||
<<"permission">> => Permission
|
||||
},
|
||||
#{<<"simple_rule">> :=
|
||||
#{<<"permission">> := NPermission} = NRule
|
||||
} = hocon_schema:check_plain(
|
||||
emqx_authz_schema,
|
||||
#{<<"simple_rule">> => Rule},
|
||||
#{},
|
||||
[simple_rule]),
|
||||
case emqx_authz:match(Client, PubSub, Topic, emqx_authz:compile(NRule)) of
|
||||
true -> {matched, NPermission};
|
||||
false -> nomatch
|
||||
end.
|
||||
|
||||
principal(CIDR, Username, ClientId) ->
|
||||
Cols = [{<<"ipaddress">>, CIDR}, {<<"username">>, Username}, {<<"clientid">>, ClientId}],
|
||||
case [#{C => V} || {C, V} <- Cols, not empty(V)] of
|
||||
[] -> throw(undefined_who);
|
||||
[Who] -> Who;
|
||||
Conds -> #{<<"and">> => Conds}
|
||||
end.
|
||||
|
||||
empty(null) -> true;
|
||||
empty("") -> true;
|
||||
empty(<<>>) -> true;
|
||||
empty(_) -> false.
|
||||
|
||||
replvar(Params, ClientInfo) ->
|
||||
replvar(Params, ClientInfo, []).
|
||||
|
||||
replvar([], _ClientInfo, Acc) ->
|
||||
lists:reverse(Acc);
|
||||
|
||||
replvar(["'%u'" | Params], ClientInfo, Acc) ->
|
||||
replvar(Params, ClientInfo, [safe_get(username, ClientInfo) | Acc]);
|
||||
replvar(["'%c'" | Params], ClientInfo = #{clientid := ClientId}, Acc) ->
|
||||
replvar(Params, ClientInfo, [ClientId | Acc]);
|
||||
replvar(["'%a'" | Params], ClientInfo = #{peerhost := IpAddr}, Acc) ->
|
||||
replvar(Params, ClientInfo, [inet_parse:ntoa(IpAddr) | Acc]);
|
||||
replvar(["'%C'" | Params], ClientInfo, Acc) ->
|
||||
replvar(Params, ClientInfo, [safe_get(cn, ClientInfo)| Acc]);
|
||||
replvar(["'%d'" | Params], ClientInfo, Acc) ->
|
||||
replvar(Params, ClientInfo, [safe_get(dn, ClientInfo)| Acc]);
|
||||
replvar([Param | Params], ClientInfo, Acc) ->
|
||||
replvar(Params, ClientInfo, [Param | Acc]).
|
||||
|
||||
safe_get(K, ClientInfo) ->
|
||||
bin(maps:get(K, ClientInfo, "undefined")).
|
||||
|
||||
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.
|
||||
|
|
@ -0,0 +1,145 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% 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_pgsql).
|
||||
|
||||
-include("emqx_authz.hrl").
|
||||
-include_lib("emqx/include/emqx.hrl").
|
||||
-include_lib("emqx/include/logger.hrl").
|
||||
|
||||
%% ACL Callbacks
|
||||
-export([ description/0
|
||||
, parse_query/1
|
||||
, check_authz/4
|
||||
]).
|
||||
|
||||
-ifdef(TEST).
|
||||
-compile(export_all).
|
||||
-compile(nowarn_export_all).
|
||||
-endif.
|
||||
|
||||
description() ->
|
||||
"AuthZ with pgsql".
|
||||
|
||||
parse_query(undefined) ->
|
||||
undefined;
|
||||
parse_query(Sql) ->
|
||||
case re:run(Sql, "'%[ucCad]'", [global, {capture, all, list}]) of
|
||||
{match, Variables} ->
|
||||
Params = [Var || [Var] <- Variables],
|
||||
Vars = ["$" ++ integer_to_list(I) || I <- lists:seq(1, length(Params))],
|
||||
NSql = lists:foldl(fun({Param, Var}, S) ->
|
||||
re:replace(S, Param, Var, [{return, list}])
|
||||
end, Sql, lists:zip(Params, Vars)),
|
||||
{NSql, Params};
|
||||
nomatch ->
|
||||
{Sql, []}
|
||||
end.
|
||||
|
||||
check_authz(Client, PubSub, Topic,
|
||||
#{<<"resource_id">> := ResourceID,
|
||||
<<"sql">> := {SQL, Params}
|
||||
}) ->
|
||||
case emqx_resource:query(ResourceID, {sql, SQL, replvar(Params, Client)}) of
|
||||
{ok, _Columns, []} -> nomatch;
|
||||
{ok, Columns, Rows} ->
|
||||
do_check_authz(Client, PubSub, Topic, Columns, Rows);
|
||||
{error, Reason} ->
|
||||
?LOG(error, "[AuthZ] Query pgsql error: ~p~n", [Reason]),
|
||||
nomatch
|
||||
end.
|
||||
|
||||
do_check_authz(_Client, _PubSub, _Topic, _Columns, []) ->
|
||||
nomatch;
|
||||
do_check_authz(Client, PubSub, Topic, Columns, [Row | Tail]) ->
|
||||
case match(Client, PubSub, Topic, format_result(Columns, Row)) of
|
||||
{matched, Permission} -> {matched, Permission};
|
||||
nomatch -> do_check_authz(Client, PubSub, Topic, Columns, Tail)
|
||||
end.
|
||||
|
||||
format_result(Columns, Row) ->
|
||||
L = [ begin
|
||||
{column, K, _, _, _, _, _, _, _} = lists:nth(I, Columns),
|
||||
V = lists:nth(I, tuple_to_list(Row)),
|
||||
{K, V}
|
||||
end || I <- lists:seq(1, length(Columns)) ],
|
||||
maps:from_list(L).
|
||||
|
||||
match(Client, PubSub, Topic,
|
||||
#{<<"permission">> := Permission,
|
||||
<<"action">> := Action,
|
||||
<<"clientid">> := ClientId,
|
||||
<<"username">> := Username,
|
||||
<<"ipaddress">> := IpAddress,
|
||||
<<"topic">> := TopicFilter
|
||||
}) ->
|
||||
Rule = #{<<"principal">> => principal(IpAddress, Username, ClientId),
|
||||
<<"topics">> => [TopicFilter],
|
||||
<<"action">> => Action,
|
||||
<<"permission">> => Permission
|
||||
},
|
||||
#{<<"simple_rule">> :=
|
||||
#{<<"permission">> := NPermission} = NRule
|
||||
} = hocon_schema:check_plain(
|
||||
emqx_authz_schema,
|
||||
#{<<"simple_rule">> => Rule},
|
||||
#{},
|
||||
[simple_rule]),
|
||||
case emqx_authz:match(Client, PubSub, Topic, emqx_authz:compile(NRule)) of
|
||||
true -> {matched, NPermission};
|
||||
false -> nomatch
|
||||
end.
|
||||
|
||||
principal(CIDR, Username, ClientId) ->
|
||||
Cols = [{<<"ipaddress">>, CIDR}, {<<"username">>, Username}, {<<"clientid">>, ClientId}],
|
||||
case [#{C => V} || {C, V} <- Cols, not empty(V)] of
|
||||
[] -> throw(undefined_who);
|
||||
[Who] -> Who;
|
||||
Conds -> #{<<"and">> => Conds}
|
||||
end.
|
||||
|
||||
empty(null) -> true;
|
||||
empty("") -> true;
|
||||
empty(<<>>) -> true;
|
||||
empty(_) -> false.
|
||||
|
||||
replvar(Params, ClientInfo) ->
|
||||
replvar(Params, ClientInfo, []).
|
||||
|
||||
replvar([], _ClientInfo, Acc) ->
|
||||
lists:reverse(Acc);
|
||||
|
||||
replvar(["'%u'" | Params], ClientInfo, Acc) ->
|
||||
replvar(Params, ClientInfo, [safe_get(username, ClientInfo) | Acc]);
|
||||
replvar(["'%c'" | Params], ClientInfo = #{clientid := ClientId}, Acc) ->
|
||||
replvar(Params, ClientInfo, [ClientId | Acc]);
|
||||
replvar(["'%a'" | Params], ClientInfo = #{peerhost := IpAddr}, Acc) ->
|
||||
replvar(Params, ClientInfo, [inet_parse:ntoa(IpAddr) | Acc]);
|
||||
replvar(["'%C'" | Params], ClientInfo, Acc) ->
|
||||
replvar(Params, ClientInfo, [safe_get(cn, ClientInfo)| Acc]);
|
||||
replvar(["'%d'" | Params], ClientInfo, Acc) ->
|
||||
replvar(Params, ClientInfo, [safe_get(dn, ClientInfo)| Acc]);
|
||||
replvar([Param | Params], ClientInfo, Acc) ->
|
||||
replvar(Params, ClientInfo, [Param | Acc]).
|
||||
|
||||
safe_get(K, ClientInfo) ->
|
||||
bin(maps:get(K, ClientInfo, "undefined")).
|
||||
|
||||
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.
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% 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_redis).
|
||||
|
||||
-include("emqx_authz.hrl").
|
||||
-include_lib("emqx/include/emqx.hrl").
|
||||
-include_lib("emqx/include/logger.hrl").
|
||||
|
||||
%% ACL Callbacks
|
||||
-export([ check_authz/4
|
||||
, description/0
|
||||
]).
|
||||
|
||||
-ifdef(TEST).
|
||||
-compile(export_all).
|
||||
-compile(nowarn_export_all).
|
||||
-endif.
|
||||
|
||||
description() ->
|
||||
"AuthZ with redis".
|
||||
|
||||
check_authz(Client, PubSub, Topic,
|
||||
#{<<"resource_id">> := ResourceID,
|
||||
<<"cmd">> := CMD
|
||||
}) ->
|
||||
NCMD = string:tokens(replvar(CMD, Client), " "),
|
||||
case emqx_resource:query(ResourceID, {cmd, NCMD}) of
|
||||
{ok, []} -> nomatch;
|
||||
{ok, Rows} ->
|
||||
do_check_authz(Client, PubSub, Topic, Rows);
|
||||
{error, Reason} ->
|
||||
?LOG(error, "[AuthZ] Query redis error: ~p", [Reason]),
|
||||
nomatch
|
||||
end.
|
||||
|
||||
do_check_authz(_Client, _PubSub, _Topic, []) ->
|
||||
nomatch;
|
||||
do_check_authz(Client, PubSub, Topic, [TopicFilter, Action | Tail]) ->
|
||||
case match(Client, PubSub, Topic,
|
||||
#{topics => TopicFilter,
|
||||
action => Action
|
||||
})
|
||||
of
|
||||
{matched, Permission} -> {matched, Permission};
|
||||
nomatch -> do_check_authz(Client, PubSub, Topic, Tail)
|
||||
end.
|
||||
|
||||
match(Client, PubSub, Topic,
|
||||
#{topics := TopicFilter,
|
||||
action := Action
|
||||
}) ->
|
||||
Rule = #{<<"principal">> => all,
|
||||
<<"topics">> => [TopicFilter],
|
||||
<<"action">> => Action,
|
||||
<<"permission">> => allow
|
||||
},
|
||||
#{<<"simple_rule">> := NRule
|
||||
} = hocon_schema:check_plain(
|
||||
emqx_authz_schema,
|
||||
#{<<"simple_rule">> => Rule},
|
||||
#{},
|
||||
[simple_rule]),
|
||||
case emqx_authz:match(Client, PubSub, Topic, emqx_authz:compile(NRule)) of
|
||||
true -> {matched, allow};
|
||||
false -> nomatch
|
||||
end.
|
||||
|
||||
replvar(Cmd, Client = #{cn := CN}) ->
|
||||
replvar(repl(Cmd, "%C", CN), maps:remove(cn, Client));
|
||||
replvar(Cmd, Client = #{dn := DN}) ->
|
||||
replvar(repl(Cmd, "%d", DN), maps:remove(dn, Client));
|
||||
replvar(Cmd, Client = #{clientid := ClientId}) ->
|
||||
replvar(repl(Cmd, "%c", ClientId), maps:remove(clientid, Client));
|
||||
replvar(Cmd, Client = #{username := Username}) ->
|
||||
replvar(repl(Cmd, "%u", Username), maps:remove(username, Client));
|
||||
replvar(Cmd, _) ->
|
||||
Cmd.
|
||||
|
||||
repl(S, _Var, undefined) ->
|
||||
S;
|
||||
repl(S, Var, Val) ->
|
||||
NVal = re:replace(Val, "&", "\\\\&", [global, {return, list}]),
|
||||
re:replace(S, Var, NVal, [{return, list}]).
|
|
@ -0,0 +1,104 @@
|
|||
-module(emqx_authz_schema).
|
||||
|
||||
-include_lib("typerefl/include/types.hrl").
|
||||
|
||||
-type action() :: publish | subscribe | all.
|
||||
-type permission() :: allow | deny.
|
||||
|
||||
-reflect_type([ permission/0
|
||||
, action/0
|
||||
]).
|
||||
|
||||
-export([structs/0, fields/1]).
|
||||
|
||||
structs() -> [authz].
|
||||
|
||||
fields(authz) ->
|
||||
[ {rules, rules()}
|
||||
];
|
||||
fields(redis_connector) ->
|
||||
[ {principal, principal()}
|
||||
, {type, #{type => hoconsc:enum([redis])}}
|
||||
, {config, #{type => map()}}
|
||||
, {cmd, query()}
|
||||
];
|
||||
fields(sql_connector) ->
|
||||
[ {principal, principal() }
|
||||
, {type, #{type => hoconsc:enum([mysql, pgsql])}}
|
||||
, {config, #{type => map()}}
|
||||
, {sql, query()}
|
||||
];
|
||||
fields(simple_rule) ->
|
||||
[ {permission, #{type => permission()}}
|
||||
, {action, #{type => action()}}
|
||||
, {topics, #{type => union_array(
|
||||
[ binary()
|
||||
, hoconsc:ref(eq_topic)
|
||||
]
|
||||
)}}
|
||||
, {principal, principal()}
|
||||
];
|
||||
fields(username) ->
|
||||
[{username, #{type => binary()}}];
|
||||
fields(clientid) ->
|
||||
[{clientid, #{type => binary()}}];
|
||||
fields(ipaddress) ->
|
||||
[{ipaddress, #{type => string()}}];
|
||||
fields(andlist) ->
|
||||
[{'and', #{type => union_array(
|
||||
[ hoconsc:ref(username)
|
||||
, hoconsc:ref(clientid)
|
||||
, hoconsc:ref(ipaddress)
|
||||
])
|
||||
}
|
||||
}
|
||||
];
|
||||
fields(orlist) ->
|
||||
[{'or', #{type => union_array(
|
||||
[ hoconsc:ref(username)
|
||||
, hoconsc:ref(clientid)
|
||||
, hoconsc:ref(ipaddress)
|
||||
])
|
||||
}
|
||||
}
|
||||
];
|
||||
fields(eq_topic) ->
|
||||
[{eq, #{type => binary()}}].
|
||||
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Internal functions
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
union_array(Item) when is_list(Item) ->
|
||||
hoconsc:array(hoconsc:union(Item)).
|
||||
|
||||
rules() ->
|
||||
#{type => union_array(
|
||||
[ hoconsc:ref(simple_rule)
|
||||
, hoconsc:ref(sql_connector)
|
||||
, hoconsc:ref(redis_connector)
|
||||
])
|
||||
}.
|
||||
|
||||
principal() ->
|
||||
#{default => all,
|
||||
type => hoconsc:union(
|
||||
[ all
|
||||
, hoconsc:ref(username)
|
||||
, hoconsc:ref(clientid)
|
||||
, hoconsc:ref(ipaddress)
|
||||
, hoconsc:ref(andlist)
|
||||
, hoconsc:ref(orlist)
|
||||
])
|
||||
}.
|
||||
|
||||
query() ->
|
||||
#{type => binary(),
|
||||
validator => fun(S) ->
|
||||
case size(S) > 0 of
|
||||
true -> ok;
|
||||
_ -> {error, "Request query"}
|
||||
end
|
||||
end
|
||||
}.
|
|
@ -0,0 +1,35 @@
|
|||
%%%-------------------------------------------------------------------
|
||||
%% @doc emqx_authz top level supervisor.
|
||||
%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
|
||||
-module(emqx_authz_sup).
|
||||
|
||||
-behaviour(supervisor).
|
||||
|
||||
-export([start_link/0]).
|
||||
|
||||
-export([init/1]).
|
||||
|
||||
-define(SERVER, ?MODULE).
|
||||
|
||||
start_link() ->
|
||||
supervisor:start_link({local, ?SERVER}, ?MODULE, []).
|
||||
|
||||
%% sup_flags() = #{strategy => strategy(), % optional
|
||||
%% intensity => non_neg_integer(), % optional
|
||||
%% period => pos_integer()} % optional
|
||||
%% child_spec() = #{id => child_id(), % mandatory
|
||||
%% start => mfargs(), % mandatory
|
||||
%% restart => restart(), % optional
|
||||
%% shutdown => shutdown(), % optional
|
||||
%% type => worker(), % optional
|
||||
%% modules => modules()} % optional
|
||||
init([]) ->
|
||||
SupFlags = #{strategy => one_for_all,
|
||||
intensity => 0,
|
||||
period => 1},
|
||||
ChildSpecs = [],
|
||||
{ok, {SupFlags, ChildSpecs}}.
|
||||
|
||||
%% internal functions
|
|
@ -0,0 +1,168 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% 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_SUITE).
|
||||
|
||||
-compile(nowarn_export_all).
|
||||
-compile(export_all).
|
||||
|
||||
-include("emqx_authz.hrl").
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
-include_lib("common_test/include/ct.hrl").
|
||||
|
||||
all() ->
|
||||
emqx_ct:all(?MODULE).
|
||||
|
||||
groups() ->
|
||||
[].
|
||||
|
||||
init_per_suite(Config) ->
|
||||
ok = emqx_ct_helpers:start_apps([emqx_authz], fun set_special_configs/1),
|
||||
Config.
|
||||
|
||||
end_per_suite(_Config) ->
|
||||
file:delete(filename:join(emqx:get_env(plugins_etc_dir), 'authz.conf')),
|
||||
emqx_ct_helpers:stop_apps([emqx_authz]).
|
||||
|
||||
set_special_configs(emqx) ->
|
||||
application:set_env(emqx, allow_anonymous, true),
|
||||
application:set_env(emqx, enable_acl_cache, false),
|
||||
application:set_env(emqx, acl_nomatch, deny),
|
||||
ok;
|
||||
set_special_configs(emqx_authz) ->
|
||||
application:set_env(emqx, plugins_etc_dir,
|
||||
emqx_ct_helpers:deps_path(emqx_authz, "test")),
|
||||
Conf = #{<<"authz">> => #{<<"rules">> => []}},
|
||||
ok = file:write_file(filename:join(emqx:get_env(plugins_etc_dir), 'authz.conf'), jsx:encode(Conf)),
|
||||
ok;
|
||||
set_special_configs(_App) ->
|
||||
ok.
|
||||
|
||||
-define(RULE1, #{<<"principal">> => all,
|
||||
<<"topics">> => [<<"#">>],
|
||||
<<"action">> => all,
|
||||
<<"permission">> => deny}
|
||||
).
|
||||
-define(RULE2, #{<<"principal">> =>
|
||||
#{<<"ipaddress">> => <<"127.0.0.1">>},
|
||||
<<"topics">> =>
|
||||
[#{<<"eq">> => <<"#">>},
|
||||
#{<<"eq">> => <<"+">>}
|
||||
] ,
|
||||
<<"action">> => all,
|
||||
<<"permission">> => allow}
|
||||
).
|
||||
-define(RULE3,#{<<"principal">> =>
|
||||
#{<<"and">> => [#{<<"username">> => "^test?"},
|
||||
#{<<"clientid">> => "^test?"}
|
||||
]},
|
||||
<<"topics">> => [<<"test">>],
|
||||
<<"action">> => publish,
|
||||
<<"permission">> => allow}
|
||||
).
|
||||
-define(RULE4,#{<<"principal">> =>
|
||||
#{<<"or">> => [#{<<"username">> => <<"^test">>},
|
||||
#{<<"clientid">> => <<"test?">>}
|
||||
]},
|
||||
<<"topics">> => [<<"%u">>,<<"%c">>],
|
||||
<<"action">> => publish,
|
||||
<<"permission">> => deny}
|
||||
).
|
||||
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
%% Testcases
|
||||
%%------------------------------------------------------------------------------
|
||||
t_compile(_) ->
|
||||
?assertEqual(#{<<"permission">> => deny,
|
||||
<<"action">> => all,
|
||||
<<"principal">> => all,
|
||||
<<"topics">> => [['#']]
|
||||
},emqx_authz:compile(?RULE1)),
|
||||
?assertEqual(#{<<"permission">> => allow,
|
||||
<<"action">> => all,
|
||||
<<"principal">> =>
|
||||
#{<<"ipaddress">> => {{127,0,0,1},{127,0,0,1},32}},
|
||||
<<"topics">> => [#{<<"eq">> => ['#']},
|
||||
#{<<"eq">> => ['+']}]
|
||||
}, emqx_authz:compile(?RULE2)),
|
||||
?assertMatch(
|
||||
#{<<"permission">> := allow,
|
||||
<<"action">> := publish,
|
||||
<<"principal">> :=
|
||||
#{<<"and">> := [#{<<"username">> := {re_pattern, _, _, _, _}},
|
||||
#{<<"clientid">> := {re_pattern, _, _, _, _}}
|
||||
]
|
||||
},
|
||||
<<"topics">> := [[<<"test">>]]
|
||||
}, emqx_authz:compile(?RULE3)),
|
||||
?assertMatch(
|
||||
#{<<"permission">> := deny,
|
||||
<<"action">> := publish,
|
||||
<<"principal">> :=
|
||||
#{<<"or">> := [#{<<"username">> := {re_pattern, _, _, _, _}},
|
||||
#{<<"clientid">> := {re_pattern, _, _, _, _}}
|
||||
]
|
||||
},
|
||||
<<"topics">> := [#{<<"pattern">> := [<<"%u">>]},
|
||||
#{<<"pattern">> := [<<"%c">>]}
|
||||
]
|
||||
}, emqx_authz:compile(?RULE4)),
|
||||
ok.
|
||||
|
||||
t_authz(_) ->
|
||||
ClientInfo1 = #{clientid => <<"test">>,
|
||||
username => <<"test">>,
|
||||
peerhost => {127,0,0,1}
|
||||
},
|
||||
ClientInfo2 = #{clientid => <<"test">>,
|
||||
username => <<"test">>,
|
||||
peerhost => {192,168,0,10}
|
||||
},
|
||||
ClientInfo3 = #{clientid => <<"test">>,
|
||||
username => <<"fake">>,
|
||||
peerhost => {127,0,0,1}
|
||||
},
|
||||
ClientInfo4 = #{clientid => <<"fake">>,
|
||||
username => <<"test">>,
|
||||
peerhost => {127,0,0,1}
|
||||
},
|
||||
|
||||
Rules1 = [emqx_authz:compile(Rule) || Rule <- [?RULE1, ?RULE2]],
|
||||
Rules2 = [emqx_authz:compile(Rule) || Rule <- [?RULE2, ?RULE1]],
|
||||
Rules3 = [emqx_authz:compile(Rule) || Rule <- [?RULE3, ?RULE4]],
|
||||
Rules4 = [emqx_authz:compile(Rule) || Rule <- [?RULE4, ?RULE1]],
|
||||
|
||||
?assertEqual(deny,
|
||||
emqx_authz:check_authz(ClientInfo1, subscribe, <<"#">>, deny, [])),
|
||||
?assertEqual({stop, deny},
|
||||
emqx_authz:check_authz(ClientInfo1, subscribe, <<"+">>, deny, Rules1)),
|
||||
?assertEqual({stop, allow},
|
||||
emqx_authz:check_authz(ClientInfo1, subscribe, <<"+">>, deny, Rules2)),
|
||||
?assertEqual({stop, allow},
|
||||
emqx_authz:check_authz(ClientInfo1, publish, <<"test">>, deny, Rules3)),
|
||||
?assertEqual({stop, deny},
|
||||
emqx_authz:check_authz(ClientInfo1, publish, <<"test">>, deny, Rules4)),
|
||||
?assertEqual({stop, deny},
|
||||
emqx_authz:check_authz(ClientInfo2, subscribe, <<"#">>, deny, Rules2)),
|
||||
?assertEqual({stop, deny},
|
||||
emqx_authz:check_authz(ClientInfo3, publish, <<"test">>, deny, Rules3)),
|
||||
?assertEqual({stop, deny},
|
||||
emqx_authz:check_authz(ClientInfo3, publish, <<"fake">>, deny, Rules4)),
|
||||
?assertEqual({stop, deny},
|
||||
emqx_authz:check_authz(ClientInfo4, publish, <<"test">>, deny, Rules3)),
|
||||
?assertEqual({stop, deny},
|
||||
emqx_authz:check_authz(ClientInfo4, publish, <<"fake">>, deny, Rules4)),
|
||||
ok.
|
|
@ -0,0 +1,130 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% 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_api_SUITE).
|
||||
|
||||
-compile(nowarn_export_all).
|
||||
-compile(export_all).
|
||||
|
||||
-include("emqx_authz.hrl").
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
-include_lib("common_test/include/ct.hrl").
|
||||
|
||||
-import(emqx_ct_http, [ request_api/3
|
||||
, request_api/5
|
||||
, get_http_data/1
|
||||
, create_default_app/0
|
||||
, delete_default_app/0
|
||||
, default_auth_header/0
|
||||
]).
|
||||
|
||||
-define(HOST, "http://127.0.0.1:8081/").
|
||||
-define(API_VERSION, "v4").
|
||||
-define(BASE_PATH, "api").
|
||||
|
||||
all() ->
|
||||
emqx_ct:all(?MODULE).
|
||||
|
||||
groups() ->
|
||||
[].
|
||||
|
||||
init_per_suite(Config) ->
|
||||
ok = emqx_ct_helpers:start_apps([emqx_authz, emqx_management], fun set_special_configs/1),
|
||||
create_default_app(),
|
||||
Config.
|
||||
|
||||
end_per_suite(_Config) ->
|
||||
delete_default_app(),
|
||||
file:delete(filename:join(emqx:get_env(plugins_etc_dir), 'authz.conf')),
|
||||
emqx_ct_helpers:stop_apps([emqx_authz, emqx_management]).
|
||||
|
||||
set_special_configs(emqx) ->
|
||||
application:set_env(emqx, allow_anonymous, true),
|
||||
application:set_env(emqx, enable_acl_cache, false),
|
||||
ok;
|
||||
set_special_configs(emqx_authz) ->
|
||||
application:set_env(emqx, plugins_etc_dir,
|
||||
emqx_ct_helpers:deps_path(emqx_authz, "test")),
|
||||
Conf = #{<<"authz">> => #{<<"rules">> => []}},
|
||||
ok = file:write_file(filename:join(emqx:get_env(plugins_etc_dir), 'authz.conf'), jsx:encode(Conf)),
|
||||
|
||||
ok;
|
||||
|
||||
set_special_configs(_App) ->
|
||||
ok.
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
%% Testcases
|
||||
%%------------------------------------------------------------------------------
|
||||
|
||||
t_api(_Config) ->
|
||||
Rule1 = #{<<"principal">> =>
|
||||
#{<<"and">> => [#{<<"username">> => <<"^test?">>},
|
||||
#{<<"clientid">> => <<"^test?">>}
|
||||
]},
|
||||
<<"action">> => <<"subscribe">>,
|
||||
<<"topics">> => [<<"%u">>],
|
||||
<<"permission">> => <<"allow">>
|
||||
},
|
||||
{ok, _} = request_http_rest_add(["authz/push"], #{rules => [Rule1]}),
|
||||
{ok, Result1} = request_http_rest_lookup(["authz"]),
|
||||
?assertMatch([Rule1 | _ ], get_http_data(Result1)),
|
||||
|
||||
Rule2 = #{<<"principal">> => #{<<"ipaddress">> => <<"127.0.0.1">>},
|
||||
<<"action">> => <<"publish">>,
|
||||
<<"topics">> => [#{<<"eq">> => <<"#">>},
|
||||
#{<<"eq">> => <<"+">>}
|
||||
],
|
||||
<<"permission">> => <<"deny">>
|
||||
},
|
||||
{ok, _} = request_http_rest_add(["authz/append"], #{rules => [Rule2]}),
|
||||
{ok, Result2} = request_http_rest_lookup(["authz"]),
|
||||
?assertEqual(Rule2#{<<"principal">> => #{<<"ipaddress">> => "127.0.0.1"}},
|
||||
lists:last(get_http_data(Result2))),
|
||||
|
||||
{ok, _} = request_http_rest_update(["authz"], #{rules => []}),
|
||||
{ok, Result3} = request_http_rest_lookup(["authz"]),
|
||||
?assertEqual([], get_http_data(Result3)),
|
||||
ok.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% HTTP Request
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
request_http_rest_list(Path) ->
|
||||
request_api(get, uri(Path), default_auth_header()).
|
||||
|
||||
request_http_rest_lookup(Path) ->
|
||||
request_api(get, uri([Path]), default_auth_header()).
|
||||
|
||||
request_http_rest_add(Path, Params) ->
|
||||
request_api(post, uri(Path), [], default_auth_header(), Params).
|
||||
|
||||
request_http_rest_update(Path, Params) ->
|
||||
request_api(put, uri([Path]), [], default_auth_header(), Params).
|
||||
|
||||
request_http_rest_delete(Login) ->
|
||||
request_api(delete, uri([Login]), default_auth_header()).
|
||||
|
||||
uri() -> uri([]).
|
||||
uri(Parts) when is_list(Parts) ->
|
||||
NParts = [b2l(E) || E <- Parts],
|
||||
?HOST ++ filename:join([?BASE_PATH, ?API_VERSION | NParts]).
|
||||
|
||||
%% @private
|
||||
b2l(B) when is_binary(B) ->
|
||||
binary_to_list(B);
|
||||
b2l(L) when is_list(L) ->
|
||||
L.
|
|
@ -0,0 +1,117 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% 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_mysql_SUITE).
|
||||
|
||||
-compile(nowarn_export_all).
|
||||
-compile(export_all).
|
||||
|
||||
-include("emqx_authz.hrl").
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
-include_lib("common_test/include/ct.hrl").
|
||||
|
||||
all() ->
|
||||
emqx_ct:all(?MODULE).
|
||||
|
||||
groups() ->
|
||||
[].
|
||||
|
||||
init_per_suite(Config) ->
|
||||
meck:new(emqx_resource, [non_strict, passthrough, no_history, no_link]),
|
||||
meck:expect(emqx_resource, check_and_create, fun(_, _, _) -> {ok, meck_data} end ),
|
||||
ok = emqx_ct_helpers:start_apps([emqx_authz], fun set_special_configs/1),
|
||||
Config.
|
||||
|
||||
end_per_suite(_Config) ->
|
||||
file:delete(filename:join(emqx:get_env(plugins_etc_dir), 'authz.conf')),
|
||||
emqx_ct_helpers:stop_apps([emqx_authz, emqx_resource]),
|
||||
meck:unload(emqx_resource).
|
||||
|
||||
set_special_configs(emqx) ->
|
||||
application:set_env(emqx, allow_anonymous, false),
|
||||
application:set_env(emqx, enable_acl_cache, false),
|
||||
application:set_env(emqx, acl_nomatch, deny),
|
||||
application:set_env(emqx, plugins_loaded_file,
|
||||
emqx_ct_helpers:deps_path(emqx, "test/loaded_plguins")),
|
||||
ok;
|
||||
set_special_configs(emqx_authz) ->
|
||||
application:set_env(emqx, plugins_etc_dir,
|
||||
emqx_ct_helpers:deps_path(emqx_authz, "test")),
|
||||
Conf = #{<<"authz">> =>
|
||||
#{<<"rules">> =>
|
||||
[#{<<"config">> =>#{<<"meck">> => <<"fake">>},
|
||||
<<"principal">> => all,
|
||||
<<"sql">> => <<"fake sql">>,
|
||||
<<"type">> => mysql}
|
||||
]}},
|
||||
ok = file:write_file(filename:join(emqx:get_env(plugins_etc_dir), 'authz.conf'), jsx:encode(Conf)),
|
||||
ok;
|
||||
set_special_configs(_App) ->
|
||||
ok.
|
||||
|
||||
-define(COLUMNS, [ <<"ipaddress">>
|
||||
, <<"username">>
|
||||
, <<"clientid">>
|
||||
, <<"action">>
|
||||
, <<"permission">>
|
||||
, <<"topic">>
|
||||
]).
|
||||
-define(RULE1, [[<<"127.0.0.1">>, <<>>, <<>>, <<"all">>, <<"deny">>, <<"#">>]]).
|
||||
-define(RULE2, [[<<"127.0.0.1">>, <<>>, <<>>, <<"all">>, <<"allow">>, <<"eq #">>]]).
|
||||
-define(RULE3, [[<<>>, <<"^test">>, <<"^test">> ,<<"subscribe">>, <<"allow">>, <<"test/%c">>]]).
|
||||
-define(RULE4, [[<<>>, <<"^test">>, <<"^test">> ,<<"publish">>, <<"allow">>, <<"test/%u">>]]).
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
%% Testcases
|
||||
%%------------------------------------------------------------------------------
|
||||
|
||||
t_authz(_) ->
|
||||
ClientInfo1 = #{clientid => <<"test">>,
|
||||
username => <<"test">>,
|
||||
peerhost => {127,0,0,1},
|
||||
zone => zone
|
||||
},
|
||||
ClientInfo2 = #{clientid => <<"test_clientid">>,
|
||||
username => <<"test_username">>,
|
||||
peerhost => {192,168,0,10},
|
||||
zone => zone
|
||||
},
|
||||
ClientInfo3 = #{clientid => <<"test_clientid">>,
|
||||
username => <<"fake_username">>,
|
||||
peerhost => {127,0,0,1},
|
||||
zone => zone
|
||||
},
|
||||
|
||||
meck:expect(emqx_resource, query, fun(_, _) -> {ok, ?COLUMNS, []} end),
|
||||
?assertEqual(deny, emqx_access_control:check_acl(ClientInfo1, subscribe, <<"#">>)), % nomatch
|
||||
?assertEqual(deny, emqx_access_control:check_acl(ClientInfo1, publish, <<"#">>)), % nomatch
|
||||
|
||||
meck:expect(emqx_resource, query, fun(_, _) -> {ok, ?COLUMNS, ?RULE1 ++ ?RULE2} end),
|
||||
?assertEqual(deny, emqx_access_control:check_acl(ClientInfo1, subscribe, <<"+">>)),
|
||||
?assertEqual(deny, emqx_access_control:check_acl(ClientInfo1, publish, <<"+">>)),
|
||||
|
||||
meck:expect(emqx_resource, query, fun(_, _) -> {ok, ?COLUMNS, ?RULE2 ++ ?RULE1} end),
|
||||
?assertEqual(allow, emqx_access_control:check_acl(ClientInfo1, subscribe, <<"#">>)),
|
||||
?assertEqual(deny, emqx_access_control:check_acl(ClientInfo1, subscribe, <<"+">>)),
|
||||
|
||||
meck:expect(emqx_resource, query, fun(_, _) -> {ok, ?COLUMNS, ?RULE3 ++ ?RULE4} end),
|
||||
?assertEqual(allow, emqx_access_control:check_acl(ClientInfo2, subscribe, <<"test/test_clientid">>)),
|
||||
?assertEqual(deny, emqx_access_control:check_acl(ClientInfo2, publish, <<"test/test_clientid">>)),
|
||||
?assertEqual(deny, emqx_access_control:check_acl(ClientInfo2, subscribe, <<"test/test_username">>)),
|
||||
?assertEqual(allow, emqx_access_control:check_acl(ClientInfo2, publish, <<"test/test_username">>)),
|
||||
?assertEqual(deny, emqx_access_control:check_acl(ClientInfo3, subscribe, <<"test">>)), % nomatch
|
||||
?assertEqual(deny, emqx_access_control:check_acl(ClientInfo3, publish, <<"test">>)), % nomatch
|
||||
ok.
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% 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_pgsql_SUITE).
|
||||
|
||||
-compile(nowarn_export_all).
|
||||
-compile(export_all).
|
||||
|
||||
-include("emqx_authz.hrl").
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
-include_lib("common_test/include/ct.hrl").
|
||||
|
||||
all() ->
|
||||
emqx_ct:all(?MODULE).
|
||||
|
||||
groups() ->
|
||||
[].
|
||||
|
||||
init_per_suite(Config) ->
|
||||
meck:new(emqx_resource, [non_strict, passthrough, no_history, no_link]),
|
||||
meck:expect(emqx_resource, check_and_create, fun(_, _, _) -> {ok, meck_data} end ),
|
||||
ok = emqx_ct_helpers:start_apps([emqx_authz], fun set_special_configs/1),
|
||||
Config.
|
||||
|
||||
end_per_suite(_Config) ->
|
||||
file:delete(filename:join(emqx:get_env(plugins_etc_dir), 'authz.conf')),
|
||||
emqx_ct_helpers:stop_apps([emqx_authz, emqx_resource]),
|
||||
meck:unload(emqx_resource).
|
||||
|
||||
set_special_configs(emqx) ->
|
||||
application:set_env(emqx, allow_anonymous, false),
|
||||
application:set_env(emqx, enable_acl_cache, false),
|
||||
application:set_env(emqx, acl_nomatch, deny),
|
||||
application:set_env(emqx, plugins_loaded_file,
|
||||
emqx_ct_helpers:deps_path(emqx, "test/loaded_plguins")),
|
||||
ok;
|
||||
set_special_configs(emqx_authz) ->
|
||||
application:set_env(emqx, plugins_etc_dir,
|
||||
emqx_ct_helpers:deps_path(emqx_authz, "test")),
|
||||
Conf = #{<<"authz">> =>
|
||||
#{<<"rules">> =>
|
||||
[#{<<"config">> =>#{<<"meck">> => <<"fake">>},
|
||||
<<"principal">> => all,
|
||||
<<"sql">> => <<"fake sql">>,
|
||||
<<"type">> => pgsql}
|
||||
]}},
|
||||
ok = file:write_file(filename:join(emqx:get_env(plugins_etc_dir), 'authz.conf'), jsx:encode(Conf)),
|
||||
ok;
|
||||
set_special_configs(_App) ->
|
||||
ok.
|
||||
|
||||
-define(COLUMNS, [ {column, <<"ipaddress">>, meck, meck, meck, meck, meck, meck, meck}
|
||||
, {column, <<"username">>, meck, meck, meck, meck, meck, meck, meck}
|
||||
, {column, <<"clientid">>, meck, meck, meck, meck, meck, meck, meck}
|
||||
, {column, <<"action">>, meck, meck, meck, meck, meck, meck, meck}
|
||||
, {column, <<"permission">>, meck, meck, meck, meck, meck, meck, meck}
|
||||
, {column, <<"topic">>, meck, meck, meck, meck, meck, meck, meck}
|
||||
]).
|
||||
-define(RULE1, [{<<"127.0.0.1">>, <<>>, <<>>, <<"all">>, <<"deny">>, <<"#">>}]).
|
||||
-define(RULE2, [{<<"127.0.0.1">>, <<>>, <<>>, <<"all">>, <<"allow">>, <<"eq #">>}]).
|
||||
-define(RULE3, [{<<>>, <<"^test">>, <<"^test">> ,<<"subscribe">>, <<"allow">>, <<"test/%c">>}]).
|
||||
-define(RULE4, [{<<>>, <<"^test">>, <<"^test">> ,<<"publish">>, <<"allow">>, <<"test/%u">>}]).
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
%% Testcases
|
||||
%%------------------------------------------------------------------------------
|
||||
|
||||
t_authz(_) ->
|
||||
ClientInfo1 = #{clientid => <<"test">>,
|
||||
username => <<"test">>,
|
||||
peerhost => {127,0,0,1},
|
||||
zone => zone
|
||||
},
|
||||
ClientInfo2 = #{clientid => <<"test_clientid">>,
|
||||
username => <<"test_username">>,
|
||||
peerhost => {192,168,0,10},
|
||||
zone => zone
|
||||
},
|
||||
ClientInfo3 = #{clientid => <<"test_clientid">>,
|
||||
username => <<"fake_username">>,
|
||||
peerhost => {127,0,0,1},
|
||||
zone => zone
|
||||
},
|
||||
|
||||
meck:expect(emqx_resource, query, fun(_, _) -> {ok, ?COLUMNS, []} end),
|
||||
?assertEqual(deny, emqx_access_control:check_acl(ClientInfo1, subscribe, <<"#">>)), % nomatch
|
||||
?assertEqual(deny, emqx_access_control:check_acl(ClientInfo1, publish, <<"#">>)), % nomatch
|
||||
|
||||
meck:expect(emqx_resource, query, fun(_, _) -> {ok, ?COLUMNS, ?RULE1 ++ ?RULE2} end),
|
||||
?assertEqual(deny, emqx_access_control:check_acl(ClientInfo1, subscribe, <<"+">>)),
|
||||
?assertEqual(deny, emqx_access_control:check_acl(ClientInfo1, publish, <<"+">>)),
|
||||
|
||||
meck:expect(emqx_resource, query, fun(_, _) -> {ok, ?COLUMNS, ?RULE2 ++ ?RULE1} end),
|
||||
?assertEqual(allow, emqx_access_control:check_acl(ClientInfo1, subscribe, <<"#">>)),
|
||||
?assertEqual(deny, emqx_access_control:check_acl(ClientInfo2, subscribe, <<"+">>)),
|
||||
|
||||
meck:expect(emqx_resource, query, fun(_, _) -> {ok, ?COLUMNS, ?RULE3 ++ ?RULE4} end),
|
||||
?assertEqual(allow, emqx_access_control:check_acl(ClientInfo2, subscribe, <<"test/test_clientid">>)),
|
||||
?assertEqual(deny, emqx_access_control:check_acl(ClientInfo2, publish, <<"test/test_clientid">>)),
|
||||
?assertEqual(deny, emqx_access_control:check_acl(ClientInfo2, subscribe, <<"test/test_username">>)),
|
||||
?assertEqual(allow, emqx_access_control:check_acl(ClientInfo2, publish, <<"test/test_username">>)),
|
||||
?assertEqual(deny, emqx_access_control:check_acl(ClientInfo3, subscribe, <<"test">>)), % nomatch
|
||||
?assertEqual(deny, emqx_access_control:check_acl(ClientInfo3, publish, <<"test">>)), % nomatch
|
||||
ok.
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% 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_redis_SUITE).
|
||||
|
||||
-compile(nowarn_export_all).
|
||||
-compile(export_all).
|
||||
|
||||
-include("emqx_authz.hrl").
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
-include_lib("common_test/include/ct.hrl").
|
||||
|
||||
all() ->
|
||||
emqx_ct:all(?MODULE).
|
||||
|
||||
groups() ->
|
||||
[].
|
||||
|
||||
init_per_suite(Config) ->
|
||||
meck:new(emqx_resource, [non_strict, passthrough, no_history, no_link]),
|
||||
meck:expect(emqx_resource, check_and_create, fun(_, _, _) -> {ok, meck_data} end ),
|
||||
ok = emqx_ct_helpers:start_apps([emqx_authz], fun set_special_configs/1),
|
||||
Config.
|
||||
|
||||
end_per_suite(_Config) ->
|
||||
file:delete(filename:join(emqx:get_env(plugins_etc_dir), 'authz.conf')),
|
||||
emqx_ct_helpers:stop_apps([emqx_authz, emqx_resource]),
|
||||
meck:unload(emqx_resource).
|
||||
|
||||
set_special_configs(emqx) ->
|
||||
application:set_env(emqx, allow_anonymous, true),
|
||||
application:set_env(emqx, enable_acl_cache, false),
|
||||
application:set_env(emqx, acl_nomatch, deny),
|
||||
application:set_env(emqx, plugins_loaded_file,
|
||||
emqx_ct_helpers:deps_path(emqx, "test/loaded_plguins")),
|
||||
ok;
|
||||
set_special_configs(emqx_authz) ->
|
||||
application:set_env(emqx, plugins_etc_dir,
|
||||
emqx_ct_helpers:deps_path(emqx_authz, "test")),
|
||||
Conf = #{<<"authz">> =>
|
||||
#{<<"rules">> =>
|
||||
[#{<<"config">> =>#{<<"meck">> => <<"fake">>},
|
||||
<<"principal">> => all,
|
||||
<<"cmd">> => <<"fake cmd">>,
|
||||
<<"type">> => redis}
|
||||
]}},
|
||||
ok = file:write_file(filename:join(emqx:get_env(plugins_etc_dir), 'authz.conf'), jsx:encode(Conf)),
|
||||
ok;
|
||||
set_special_configs(_App) ->
|
||||
ok.
|
||||
|
||||
-define(RULE1, [<<"test/%u">>, <<"publish">>]).
|
||||
-define(RULE2, [<<"test/%c">>, <<"publish">>]).
|
||||
-define(RULE3, [<<"#">>, <<"subscribe">>]).
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
%% Testcases
|
||||
%%------------------------------------------------------------------------------
|
||||
|
||||
t_authz(_) ->
|
||||
ClientInfo = #{clientid => <<"clientid">>,
|
||||
username => <<"username">>,
|
||||
peerhost => {127,0,0,1},
|
||||
zone => zone
|
||||
},
|
||||
|
||||
meck:expect(emqx_resource, query, fun(_, _) -> {ok, []} end),
|
||||
% nomatch
|
||||
?assertEqual(deny,
|
||||
emqx_access_control:check_acl(ClientInfo, subscribe, <<"#">>)),
|
||||
?assertEqual(deny,
|
||||
emqx_access_control:check_acl(ClientInfo, publish, <<"#">>)),
|
||||
|
||||
|
||||
meck:expect(emqx_resource, query, fun(_, _) -> {ok, ?RULE1 ++ ?RULE2} end),
|
||||
% nomatch
|
||||
?assertEqual(deny,
|
||||
emqx_access_control:check_acl(ClientInfo, subscribe, <<"+">>)),
|
||||
% nomatch
|
||||
?assertEqual(deny,
|
||||
emqx_access_control:check_acl(ClientInfo, subscribe, <<"test/username">>)),
|
||||
|
||||
?assertEqual(allow,
|
||||
emqx_access_control:check_acl(ClientInfo, publish, <<"test/clientid">>)),
|
||||
?assertEqual(allow,
|
||||
emqx_access_control:check_acl(ClientInfo, publish, <<"test/clientid">>)),
|
||||
|
||||
meck:expect(emqx_resource, query, fun(_, _) -> {ok, ?RULE3} end),
|
||||
|
||||
?assertEqual(allow,
|
||||
emqx_access_control:check_acl(ClientInfo, subscribe, <<"#">>)),
|
||||
% nomatch
|
||||
?assertEqual(deny,
|
||||
emqx_access_control:check_acl(ClientInfo, publish, <<"#">>)),
|
||||
ok.
|
||||
|
|
@ -84,10 +84,15 @@ listener_name(Proto) ->
|
|||
list_to_atom(atom_to_list(Proto) ++ ":management").
|
||||
|
||||
http_handlers() ->
|
||||
Apps = [ App || {App, _, _} <- application:which_applications(),
|
||||
case re:run(atom_to_list(App), "^emqx") of
|
||||
{match,[{0,4}]} -> true;
|
||||
_ -> false
|
||||
end],
|
||||
Plugins = lists:map(fun(Plugin) -> Plugin#plugin.name end, emqx_plugins:list()),
|
||||
[{"/api/v4", minirest:handler(#{apps => Plugins ++ [emqx_modules] -- ?EXCEPT_PLUGIN,
|
||||
[{"/api/v4", minirest:handler(#{apps => Plugins ++ Apps -- ?EXCEPT_PLUGIN,
|
||||
except => ?EXCEPT,
|
||||
filter => fun filter/1}),
|
||||
filter => fun(_) -> true end}),
|
||||
[{authorization, fun authorize_appid/1}]}].
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
|
@ -119,19 +124,6 @@ authorize_appid(Req) ->
|
|||
_ -> false
|
||||
end.
|
||||
|
||||
-ifdef(EMQX_ENTERPRISE).
|
||||
filter(_) ->
|
||||
true.
|
||||
-else.
|
||||
filter(#{app := emqx_modules}) -> true;
|
||||
filter(#{app := App}) ->
|
||||
case emqx_plugins:find_plugin(App) of
|
||||
false -> false;
|
||||
Plugin -> Plugin#plugin.active
|
||||
end.
|
||||
-endif.
|
||||
|
||||
|
||||
format(Port) when is_integer(Port) ->
|
||||
io_lib:format("0.0.0.0:~w", [Port]);
|
||||
format({Addr, Port}) when is_list(Addr) ->
|
||||
|
|
|
@ -1,2 +1 @@
|
|||
{emqx_mod_acl_internal, true}.
|
||||
{emqx_mod_presence, true}.
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
{edoc_opts, [{preprocess,true}]}.
|
||||
{erl_opts, [warn_unused_vars,warn_shadow_vars,warn_unused_import,
|
||||
warn_obsolete_guard,compressed,
|
||||
warn_obsolete_guard,compressed, nowarn_unused_import,
|
||||
{d, snk_kind, msg}]}.
|
||||
|
||||
{xref_checks,[undefined_function_calls,undefined_functions,locals_not_used,
|
||||
|
|
|
@ -249,6 +249,7 @@ relx_apps(ReleaseType) ->
|
|||
, {mnesia, load}
|
||||
, {ekka, load}
|
||||
, {emqx_plugin_libs, load}
|
||||
, emqx_authz
|
||||
, observer_cli
|
||||
, emqx_http_lib
|
||||
, emqx_resource
|
||||
|
@ -386,6 +387,7 @@ emqx_etc_overlay_common() ->
|
|||
{"{{base_dir}}/lib/emqx/etc/emqx.conf", "etc/emqx.conf"},
|
||||
{"{{base_dir}}/lib/emqx/etc/ssl_dist.conf", "etc/ssl_dist.conf"},
|
||||
{"{{base_dir}}/lib/emqx_data_bridge/etc/emqx_data_bridge.conf", "etc/plugins/emqx_data_bridge.conf"},
|
||||
{"{{base_dir}}/lib/emqx_authz/etc/emqx_authz.conf", "etc/plugins/authz.conf"},
|
||||
%% TODO: check why it has to end with .paho
|
||||
%% and why it is put to etc/plugins dir
|
||||
{"{{base_dir}}/lib/emqx/etc/acl.conf.paho", "etc/plugins/acl.conf.paho"}].
|
||||
|
|
Loading…
Reference in New Issue