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").
|
list_to_atom(atom_to_list(Proto) ++ ":management").
|
||||||
|
|
||||||
http_handlers() ->
|
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()),
|
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,
|
except => ?EXCEPT,
|
||||||
filter => fun filter/1}),
|
filter => fun(_) -> true end}),
|
||||||
[{authorization, fun authorize_appid/1}]}].
|
[{authorization, fun authorize_appid/1}]}].
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
@ -119,19 +124,6 @@ authorize_appid(Req) ->
|
||||||
_ -> false
|
_ -> false
|
||||||
end.
|
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) ->
|
format(Port) when is_integer(Port) ->
|
||||||
io_lib:format("0.0.0.0:~w", [Port]);
|
io_lib:format("0.0.0.0:~w", [Port]);
|
||||||
format({Addr, Port}) when is_list(Addr) ->
|
format({Addr, Port}) when is_list(Addr) ->
|
||||||
|
|
|
@ -1,2 +1 @@
|
||||||
{emqx_mod_acl_internal, true}.
|
|
||||||
{emqx_mod_presence, true}.
|
{emqx_mod_presence, true}.
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
{edoc_opts, [{preprocess,true}]}.
|
{edoc_opts, [{preprocess,true}]}.
|
||||||
{erl_opts, [warn_unused_vars,warn_shadow_vars,warn_unused_import,
|
{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}]}.
|
{d, snk_kind, msg}]}.
|
||||||
|
|
||||||
{xref_checks,[undefined_function_calls,undefined_functions,locals_not_used,
|
{xref_checks,[undefined_function_calls,undefined_functions,locals_not_used,
|
||||||
|
|
|
@ -249,6 +249,7 @@ relx_apps(ReleaseType) ->
|
||||||
, {mnesia, load}
|
, {mnesia, load}
|
||||||
, {ekka, load}
|
, {ekka, load}
|
||||||
, {emqx_plugin_libs, load}
|
, {emqx_plugin_libs, load}
|
||||||
|
, emqx_authz
|
||||||
, observer_cli
|
, observer_cli
|
||||||
, emqx_http_lib
|
, emqx_http_lib
|
||||||
, emqx_resource
|
, 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/emqx.conf", "etc/emqx.conf"},
|
||||||
{"{{base_dir}}/lib/emqx/etc/ssl_dist.conf", "etc/ssl_dist.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_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
|
%% TODO: check why it has to end with .paho
|
||||||
%% and why it is put to etc/plugins dir
|
%% and why it is put to etc/plugins dir
|
||||||
{"{{base_dir}}/lib/emqx/etc/acl.conf.paho", "etc/plugins/acl.conf.paho"}].
|
{"{{base_dir}}/lib/emqx/etc/acl.conf.paho", "etc/plugins/acl.conf.paho"}].
|
||||||
|
|
Loading…
Reference in New Issue