Merge branch 'master' into cluster-call-api

This commit is contained in:
zhongwencool 2021-09-10 13:11:53 +08:00
commit 5abffe4cfe
236 changed files with 14069 additions and 10684 deletions

View File

@ -91,6 +91,12 @@ emqx_test(){
;;
"rpm")
packagename=$(basename "${PACKAGE_PATH}/${EMQX_NAME}"-*.rpm)
if [[ "${ARCH}" == "amd64" && $(rpm -E '%{rhel}') == 7 ]] ;
then
# EMQX OTP requires openssl11 to have TLS1.3 support
yum install -y openssl11;
fi
rpm -ivh "${PACKAGE_PATH}/${packagename}"
if ! rpm -q emqx | grep -q emqx; then
echo "package install error"
@ -126,7 +132,7 @@ export EMQX_LOG__FILE_HANDLERS__DEFAULT__LEVEL=debug
EOF
## for ARM, due to CI env issue, skip start of quic listener for the moment
[[ $(arch) == *arm* || $(arch) == aarch64 ]] && tee -a "$emqx_env_vars" <<EOF
export EMQX_ZONES__DEFAULT__LISTENERS__MQTT_QUIC__ENABLED=false
export EMQX_LISTENERS__QUIC__DEFAULT__ENABLED=false
EOF
else
echo "Error: cannot locate emqx_vars"

View File

@ -2,7 +2,7 @@
name: Bug Report
about: Create a report to help us improve
title: ''
labels: Support
labels: "Support, needs-triage"
---

View File

@ -2,7 +2,7 @@
name: Feature Request
about: Suggest an idea for this project
title: ''
labels: Feature
labels: "Feature, needs-triage"
---

View File

@ -2,7 +2,7 @@
name: Support Needed
about: Asking a question about usages, docs or anything you're insterested in
title: ''
labels: Support
labels: "Support, needs-triage"
---

102
.github/workflows/run_api_tests.yaml vendored Normal file
View File

@ -0,0 +1,102 @@
name: API Test Suite
on:
push:
tags:
- e*
- v*
pull_request:
jobs:
build:
runs-on: ubuntu-latest
container: "emqx/build-env:erl23.2.7.2-emqx-2-ubuntu20.04"
steps:
- uses: actions/checkout@v2
- name: zip emqx-broker
if: endsWith(github.repository, 'emqx')
run: |
make emqx-zip
- name: zip emqx-broker
if: endsWith(github.repository, 'enterprise')
run: |
echo "https://ci%40emqx.io:${{ secrets.CI_GIT_TOKEN }}@github.com" > $HOME/.git-credentials
git config --global credential.helper store
make emqx-ee-zip
- uses: actions/upload-artifact@v2
with:
name: emqx-broker
path: _packages/**/*.zip
api-test:
needs: build
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
script_name:
- api_metrics
- api_subscriptions
steps:
- uses: actions/checkout@v2
with:
repository: emqx/emqx-fvt
path: .
- uses: actions/setup-java@v1
with:
java-version: '8.0.282' # The JDK version to make available on the path.
java-package: jdk # (jre, jdk, or jdk+fx) - defaults to jdk
architecture: x64 # (x64 or x86) - defaults to x64
- uses: actions/download-artifact@v2
with:
name: emqx-broker
path: .
- name: start emqx-broker
env:
EMQX_LISTENERS__WSS__DEFAULT__BIND: "0.0.0.0:8085"
run: |
unzip ./emqx/*.zip
./emqx/bin/emqx start
- name: install jmeter
timeout-minutes: 10
env:
JMETER_VERSION: 5.3
run: |
wget --no-verbose --no-check-certificate -O /tmp/apache-jmeter.tgz https://downloads.apache.org/jmeter/binaries/apache-jmeter-$JMETER_VERSION.tgz
cd /tmp && tar -xvf apache-jmeter.tgz
echo "jmeter.save.saveservice.output_format=xml" >> /tmp/apache-jmeter-$JMETER_VERSION/user.properties
echo "jmeter.save.saveservice.response_data.on_error=true" >> /tmp/apache-jmeter-$JMETER_VERSION/user.properties
wget --no-verbose -O /tmp/apache-jmeter-$JMETER_VERSION/lib/ext/mqtt-xmeter-2.0.2-jar-with-dependencies.jar https://raw.githubusercontent.com/xmeter-net/mqtt-jmeter/master/Download/v2.0.2/mqtt-xmeter-2.0.2-jar-with-dependencies.jar
ln -s /tmp/apache-jmeter-$JMETER_VERSION /opt/jmeter
- name: run ${{ matrix.script_name }}
run: |
/opt/jmeter/bin/jmeter.sh \
-Jjmeter.save.saveservice.output_format=xml -n \
-t .ci/api-test-suite/${{ matrix.script_name }}.jmx \
-Demqx_ip="127.0.0.1" \
-l jmeter_logs/${{ matrix.script_name }}.jtl \
-j jmeter_logs/logs/${{ matrix.script_name }}.log
- name: check test logs
run: |
if cat jmeter_logs/${{ matrix.script_name }}.jtl | grep -e '<failure>true</failure>' > /dev/null 2>&1; then
grep -A 5 -B 3 '<failure>true</failure>' jmeter_logs/${{ matrix.script_name }}.jtl > jmeter_logs/${{ matrix.script_name }}_err_api.txt
echo "check logs failed"
exit 1
fi
- uses: actions/upload-artifact@v1
if: failure()
with:
name: jmeter_logs
path: ./jmeter_logs
- uses: actions/upload-artifact@v1
if: failure()
with:
name: jmeter_logs
path: emqx/log
delete-package:
runs-on: ubuntu-20.04
needs: api-test
if: always()
steps:
- uses: geekyeggo/delete-artifact@v1
with:
name: emqx-broker

View File

@ -5,7 +5,7 @@ BUILD = $(CURDIR)/build
SCRIPTS = $(CURDIR)/scripts
export PKG_VSN ?= $(shell $(CURDIR)/pkg-vsn.sh)
export EMQX_DESC ?= EMQ X
export EMQX_DASHBOARD_VERSION ?= v5.0.0-beta.9
export EMQX_DASHBOARD_VERSION ?= v5.0.0-beta.11
ifeq ($(OS),Windows_NT)
export REBAR_COLOR=none
endif

View File

@ -4,7 +4,7 @@
[![Build Status](https://travis-ci.org/emqx/emqx.svg)](https://travis-ci.org/emqx/emqx)
[![Coverage Status](https://coveralls.io/repos/github/emqx/emqx/badge.svg)](https://coveralls.io/github/emqx/emqx)
[![Docker Pulls](https://img.shields.io/docker/pulls/emqx/emqx)](https://hub.docker.com/r/emqx/emqx)
[![Slack Invite](<https://slack-invite.emqx.io/badge.svg>)](https://slack-invite.emqx.io)
[![Slack](https://img.shields.io/badge/Slack-EMQ%20X-39AE85?logo=slack)](https://slack-invite.emqx.io/)
[![Twitter](https://img.shields.io/badge/Twitter-EMQ-1DA1F2?logo=twitter)](https://twitter.com/EMQTech)
[![Community](https://img.shields.io/badge/Community-EMQ%20X-yellow)](https://askemq.com)
[![YouTube](https://img.shields.io/badge/Subscribe-EMQ%20中文-FF0000?logo=youtube)](https://www.youtube.com/channel/UCir_r04HIsLjf2qqyZ4A8Cg)
@ -90,7 +90,7 @@ make eunit ct
### 执行部分应用的 common tests
```bash
make apps/emqx_bridge_mqtt-ct
make apps/emqx_retainer-ct
```
### 静态分析(Dialyzer)

View File

@ -4,7 +4,7 @@
[![Build Status](https://travis-ci.org/emqx/emqx.svg)](https://travis-ci.org/emqx/emqx)
[![Coverage Status](https://coveralls.io/repos/github/emqx/emqx/badge.svg)](https://coveralls.io/github/emqx/emqx)
[![Docker Pulls](https://img.shields.io/docker/pulls/emqx/emqx)](https://hub.docker.com/r/emqx/emqx)
[![Slack Invite](<https://slack-invite.emqx.io/badge.svg>)](https://slack-invite.emqx.io)
[![Slack](https://img.shields.io/badge/Slack-EMQ%20X-39AE85?logo=slack)](https://slack-invite.emqx.io/)
[![Twitter](https://img.shields.io/badge/Twitter-EMQ-1DA1F2?logo=twitter)](https://twitter.com/EMQTech)
[![YouTube](https://img.shields.io/badge/Subscribe-EMQ-FF0000?logo=youtube)](https://www.youtube.com/channel/UC5FjR77ErAxvZENEWzQaO5Q)
@ -84,7 +84,7 @@ make eunit ct
### common test の一部を実行する
```bash
make apps/emqx_bridge_mqtt-ct
make apps/emqx_retainer-ct
```
### Dialyzer

View File

@ -4,7 +4,7 @@
[![Build Status](https://travis-ci.org/emqx/emqx.svg)](https://travis-ci.org/emqx/emqx)
[![Coverage Status](https://coveralls.io/repos/github/emqx/emqx/badge.svg?branch=master)](https://coveralls.io/github/emqx/emqx?branch=master)
[![Docker Pulls](https://img.shields.io/docker/pulls/emqx/emqx)](https://hub.docker.com/r/emqx/emqx)
[![Slack Invite](<https://slack-invite.emqx.io/badge.svg>)](https://slack-invite.emqx.io)
[![Slack](https://img.shields.io/badge/Slack-EMQ%20X-39AE85?logo=slack)](https://slack-invite.emqx.io/)
[![Twitter](https://img.shields.io/badge/Follow-EMQ-1DA1F2?logo=twitter)](https://twitter.com/EMQTech)
[![Community](https://img.shields.io/badge/Community-EMQ%20X-yellow?logo=github)](https://github.com/emqx/emqx/discussions)
[![YouTube](https://img.shields.io/badge/Subscribe-EMQ-FF0000?logo=youtube)](https://www.youtube.com/channel/UC5FjR77ErAxvZENEWzQaO5Q)
@ -93,7 +93,7 @@ make eunit ct
Пример:
```bash
make apps/emqx_bridge_mqtt-ct
make apps/emqx_retainer-ct
```
### Dialyzer

View File

@ -4,7 +4,7 @@
[![Build Status](https://travis-ci.org/emqx/emqx.svg)](https://travis-ci.org/emqx/emqx)
[![Coverage Status](https://coveralls.io/repos/github/emqx/emqx/badge.svg?branch=master)](https://coveralls.io/github/emqx/emqx?branch=master)
[![Docker Pulls](https://img.shields.io/docker/pulls/emqx/emqx)](https://hub.docker.com/r/emqx/emqx)
[![Slack Invite](<https://slack-invite.emqx.io/badge.svg>)](https://slack-invite.emqx.io)
[![Slack](https://img.shields.io/badge/Slack-EMQ%20X-39AE85?logo=slack)](https://slack-invite.emqx.io/)
[![Twitter](https://img.shields.io/badge/Follow-EMQ-1DA1F2?logo=twitter)](https://twitter.com/EMQTech)
[![YouTube](https://img.shields.io/badge/Subscribe-EMQ-FF0000?logo=youtube)](https://www.youtube.com/channel/UC5FjR77ErAxvZENEWzQaO5Q)
@ -92,7 +92,7 @@ make eunit ct
Examples
```bash
make apps/emqx_bridge_mqtt-ct
make apps/emqx_retainer-ct
```
### Dialyzer

View File

@ -64,7 +64,7 @@ listeners.tcp.default {
proxy_protocol = false
## Sets the timeout for proxy protocol. EMQ X will close the TCP connection
## if no proxy protocol packet recevied within the timeout.
## if no proxy protocol packet received within the timeout.
##
## @doc listeners.tcp.<name>.proxy_protocol_timeout
## ValueType: Duration
@ -163,7 +163,7 @@ listeners.ssl.default {
proxy_protocol = false
## Sets the timeout for proxy protocol. EMQ X will close the TCP connection
## if no proxy protocol packet recevied within the timeout.
## if no proxy protocol packet received within the timeout.
##
## @doc listeners.ssl.<name>.proxy_protocol_timeout
## ValueType: Duration
@ -345,7 +345,7 @@ listeners.ws.default {
proxy_protocol = false
## Sets the timeout for proxy protocol. EMQ X will close the TCP connection
## if no proxy protocol packet recevied within the timeout.
## if no proxy protocol packet received within the timeout.
##
## @doc listeners.ws.<name>.proxy_protocol_timeout
## ValueType: Duration
@ -448,7 +448,7 @@ listeners.wss.default {
proxy_protocol = false
## Sets the timeout for proxy protocol. EMQ X will close the TCP connection
## if no proxy protocol packet recevied within the timeout.
## if no proxy protocol packet received within the timeout.
##
## @doc listeners.wss.<name>.proxy_protocol_timeout
## ValueType: Duration

View File

@ -134,3 +134,19 @@
}).
-endif.
%%--------------------------------------------------------------------
%% Authentication
%%--------------------------------------------------------------------
-record(authenticator,
{ id :: binary()
, provider :: module()
, enable :: boolean()
, state :: map()
}).
-record(chain,
{ name :: atom()
, authenticators :: [#authenticator{}]
}).

View File

@ -15,7 +15,7 @@
, {esockd, {git, "https://github.com/emqx/esockd", {tag, "5.8.2"}}}
, {ekka, {git, "https://github.com/emqx/ekka", {tag, "0.10.8"}}}
, {gen_rpc, {git, "https://github.com/emqx/gen_rpc", {tag, "2.5.1"}}}
, {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.14.0"}}}
, {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.15.0"}}}
, {pbkdf2, {git, "https://github.com/emqx/erlang-pbkdf2.git", {tag, "2.0.4"}}}
, {recon, {git, "https://github.com/ferd/recon", {tag, "2.5.1"}}}
, {snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "0.14.1"}}}

View File

@ -29,9 +29,9 @@
-spec(authenticate(emqx_types:clientinfo()) ->
{ok, map()} | {ok, map(), binary()} | {continue, map()} | {continue, binary(), map()} | {error, term()}).
authenticate(Credential) ->
case run_hooks('client.authenticate', [Credential], {ok, #{superuser => false}}) of
case run_hooks('client.authenticate', [Credential], {ok, #{is_superuser => false}}) of
ok ->
{ok, #{superuser => false}};
{ok, #{is_superuser => false}};
Other ->
Other
end.

View File

@ -159,6 +159,7 @@ format(#activated_alarm{name = Name, message = Message, activate_at = At, detail
name => Name,
message => Message,
duration => (Now - At) div 1000, %% to millisecond
activate_at => to_rfc3339(At),
details => Details
};
format(#deactivated_alarm{name = Name, message = Message, activate_at = At, details = Details,
@ -168,11 +169,16 @@ format(#deactivated_alarm{name = Name, message = Message, activate_at = At, deta
name => Name,
message => Message,
duration => DAt - At,
activate_at => to_rfc3339(At),
deactivate_at => to_rfc3339(DAt),
details => Details
};
format(_) ->
{error, unknow_alarm}.
to_rfc3339(Timestamp) ->
list_to_binary(calendar:system_time_to_rfc3339(Timestamp div 1000, [{unit, millisecond}])).
%%--------------------------------------------------------------------
%% gen_server callbacks
%%--------------------------------------------------------------------

View File

@ -0,0 +1,735 @@
%%--------------------------------------------------------------------
%% Copyright (c) 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_authentication).
-behaviour(gen_server).
-behaviour(hocon_schema).
-behaviour(emqx_config_handler).
-include("emqx.hrl").
-include("logger.hrl").
-export([ roots/0
, fields/1
]).
-export([ pre_config_update/2
, post_config_update/4
]).
-export([ authenticate/2
]).
-export([ initialize_authentication/2 ]).
-export([ start_link/0
, stop/0
]).
-export([ add_provider/2
, remove_provider/1
, create_chain/1
, delete_chain/1
, lookup_chain/1
, list_chains/0
, create_authenticator/2
, delete_authenticator/2
, update_authenticator/3
, lookup_authenticator/2
, list_authenticators/1
, move_authenticator/3
]).
-export([ import_users/3
, add_user/3
, delete_user/3
, update_user/4
, lookup_user/3
, list_users/2
]).
-export([ generate_id/1 ]).
%% gen_server callbacks
-export([ init/1
, handle_call/3
, handle_cast/2
, handle_info/2
, terminate/2
, code_change/3
]).
-define(CHAINS_TAB, emqx_authn_chains).
-define(VER_1, <<"1">>).
-define(VER_2, <<"2">>).
-type config() :: #{atom() => term()}.
-type state() :: #{atom() => term()}.
-type extra() :: #{is_superuser := boolean(),
atom() => term()}.
-type user_info() :: #{user_id := binary(),
atom() => term()}.
-callback refs() -> [{ref, Module, Name}] when Module::module(), Name::atom().
-callback create(Config)
-> {ok, State}
| {error, term()}
when Config::config(), State::state().
-callback update(Config, State)
-> {ok, NewState}
| {error, term()}
when Config::config(), State::state(), NewState::state().
-callback authenticate(Credential, State)
-> ignore
| {ok, Extra}
| {ok, Extra, AuthData}
| {continue, AuthCache}
| {continue, AuthData, AuthCache}
| {error, term()}
when Credential::map(), State::state(), Extra::extra(), AuthData::binary(), AuthCache::map().
-callback destroy(State)
-> ok
when State::state().
-callback import_users(Filename, State)
-> ok
| {error, term()}
when Filename::binary(), State::state().
-callback add_user(UserInfo, State)
-> {ok, User}
| {error, term()}
when UserInfo::user_info(), State::state(), User::user_info().
-callback delete_user(UserID, State)
-> ok
| {error, term()}
when UserID::binary(), State::state().
-callback update_user(UserID, UserInfo, State)
-> {ok, User}
| {error, term()}
when UserID::binary, UserInfo::map(), State::state(), User::user_info().
-callback list_users(State)
-> {ok, Users}
when State::state(), Users::[user_info()].
-optional_callbacks([ import_users/2
, add_user/2
, delete_user/2
, update_user/3
, list_users/1
]).
%%------------------------------------------------------------------------------
%% Hocon Schema
%%------------------------------------------------------------------------------
roots() -> [{authentication, fun authentication/1}].
fields(_) -> [].
authentication(type) ->
{ok, Refs} = get_refs(),
hoconsc:union([hoconsc:array(hoconsc:union(Refs)) | Refs]);
authentication(default) -> [];
authentication(_) -> undefined.
%%------------------------------------------------------------------------------
%% Callbacks of config handler
%%------------------------------------------------------------------------------
pre_config_update(UpdateReq, OldConfig) ->
case do_pre_config_update(UpdateReq, to_list(OldConfig)) of
{error, Reason} -> {error, Reason};
{ok, NewConfig} -> {ok, may_to_map(NewConfig)}
end.
do_pre_config_update({create_authenticator, _ChainName, Config}, OldConfig) ->
{ok, OldConfig ++ [Config]};
do_pre_config_update({delete_authenticator, _ChainName, AuthenticatorID}, OldConfig) ->
NewConfig = lists:filter(fun(OldConfig0) ->
AuthenticatorID =/= generate_id(OldConfig0)
end, OldConfig),
{ok, NewConfig};
do_pre_config_update({update_authenticator, _ChainName, AuthenticatorID, Config}, OldConfig) ->
NewConfig = lists:map(fun(OldConfig0) ->
case AuthenticatorID =:= generate_id(OldConfig0) of
true -> maps:merge(OldConfig0, Config);
false -> OldConfig0
end
end, OldConfig),
{ok, NewConfig};
do_pre_config_update({move_authenticator, _ChainName, AuthenticatorID, Position}, OldConfig) ->
case split_by_id(AuthenticatorID, OldConfig) of
{error, Reason} -> {error, Reason};
{ok, Part1, [Found | Part2]} ->
case Position of
<<"top">> ->
{ok, [Found | Part1] ++ Part2};
<<"bottom">> ->
{ok, Part1 ++ Part2 ++ [Found]};
<<"before:", Before/binary>> ->
case split_by_id(Before, Part1 ++ Part2) of
{error, Reason} ->
{error, Reason};
{ok, NPart1, [NFound | NPart2]} ->
{ok, NPart1 ++ [Found, NFound | NPart2]}
end;
_ ->
{error, {invalid_parameter, position}}
end
end.
post_config_update(UpdateReq, NewConfig, OldConfig, AppEnvs) ->
do_post_config_update(UpdateReq, check_config(to_list(NewConfig)), OldConfig, AppEnvs).
do_post_config_update({create_authenticator, ChainName, Config}, _NewConfig, _OldConfig, _AppEnvs) ->
NConfig = check_config(Config),
_ = create_chain(ChainName),
create_authenticator(ChainName, NConfig);
do_post_config_update({delete_authenticator, ChainName, AuthenticatorID}, _NewConfig, _OldConfig, _AppEnvs) ->
delete_authenticator(ChainName, AuthenticatorID);
do_post_config_update({update_authenticator, ChainName, AuthenticatorID, _Config}, NewConfig, _OldConfig, _AppEnvs) ->
[Config] = lists:filter(fun(NewConfig0) ->
AuthenticatorID =:= generate_id(NewConfig0)
end, NewConfig),
NConfig = check_config(Config),
update_authenticator(ChainName, AuthenticatorID, NConfig);
do_post_config_update({move_authenticator, ChainName, AuthenticatorID, Position}, _NewConfig, _OldConfig, _AppEnvs) ->
NPosition = case Position of
<<"top">> -> top;
<<"bottom">> -> bottom;
<<"before:", Before/binary>> ->
{before, Before}
end,
move_authenticator(ChainName, AuthenticatorID, NPosition).
check_config(Config) ->
#{authentication := CheckedConfig} = hocon_schema:check_plain(emqx_authentication,
#{<<"authentication">> => Config}, #{nullable => true, atom_key => true}),
CheckedConfig.
%%------------------------------------------------------------------------------
%% Authenticate
%%------------------------------------------------------------------------------
authenticate(#{listener := Listener, protocol := Protocol} = Credential, _AuthResult) ->
case ets:lookup(?CHAINS_TAB, Listener) of
[#chain{authenticators = Authenticators}] when Authenticators =/= [] ->
do_authenticate(Authenticators, Credential);
_ ->
case ets:lookup(?CHAINS_TAB, global_chain(Protocol)) of
[#chain{authenticators = Authenticators}] when Authenticators =/= [] ->
do_authenticate(Authenticators, Credential);
_ ->
ignore
end
end.
do_authenticate([], _) ->
{stop, {error, not_authorized}};
do_authenticate([#authenticator{provider = Provider, state = State} | More], Credential) ->
case Provider:authenticate(Credential, State) of
ignore ->
do_authenticate(More, Credential);
Result ->
%% {ok, Extra}
%% {ok, Extra, AuthData}
%% {continue, AuthCache}
%% {continue, AuthData, AuthCache}
%% {error, Reason}
{stop, Result}
end.
%%------------------------------------------------------------------------------
%% APIs
%%------------------------------------------------------------------------------
initialize_authentication(_, []) ->
ok;
initialize_authentication(ChainName, AuthenticatorsConfig) ->
_ = create_chain(ChainName),
CheckedConfig = check_config(to_list(AuthenticatorsConfig)),
lists:foreach(fun(AuthenticatorConfig) ->
case create_authenticator(ChainName, AuthenticatorConfig) of
{ok, _} ->
ok;
{error, Reason} ->
?LOG(error, "Failed to create authenticator '~s': ~p", [generate_id(AuthenticatorConfig), Reason])
end
end, CheckedConfig).
start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
stop() ->
gen_server:stop(?MODULE).
get_refs() ->
gen_server:call(?MODULE, get_refs).
add_provider(AuthNType, Provider) ->
gen_server:call(?MODULE, {add_provider, AuthNType, Provider}).
remove_provider(AuthNType) ->
gen_server:call(?MODULE, {remove_provider, AuthNType}).
create_chain(Name) ->
gen_server:call(?MODULE, {create_chain, Name}).
delete_chain(Name) ->
gen_server:call(?MODULE, {delete_chain, Name}).
lookup_chain(Name) ->
gen_server:call(?MODULE, {lookup_chain, Name}).
list_chains() ->
Chains = ets:tab2list(?CHAINS_TAB),
{ok, [serialize_chain(Chain) || Chain <- Chains]}.
create_authenticator(ChainName, Config) ->
gen_server:call(?MODULE, {create_authenticator, ChainName, Config}).
delete_authenticator(ChainName, AuthenticatorID) ->
gen_server:call(?MODULE, {delete_authenticator, ChainName, AuthenticatorID}).
update_authenticator(ChainName, AuthenticatorID, Config) ->
gen_server:call(?MODULE, {update_authenticator, ChainName, AuthenticatorID, Config}).
lookup_authenticator(ChainName, AuthenticatorID) ->
case ets:lookup(?CHAINS_TAB, ChainName) of
[] ->
{error, {not_found, {chain, ChainName}}};
[#chain{authenticators = Authenticators}] ->
case lists:keyfind(AuthenticatorID, #authenticator.id, Authenticators) of
false ->
{error, {not_found, {authenticator, AuthenticatorID}}};
Authenticator ->
{ok, serialize_authenticator(Authenticator)}
end
end.
list_authenticators(ChainName) ->
case ets:lookup(?CHAINS_TAB, ChainName) of
[] ->
{error, {not_found, {chain, ChainName}}};
[#chain{authenticators = Authenticators}] ->
{ok, serialize_authenticators(Authenticators)}
end.
move_authenticator(ChainName, AuthenticatorID, Position) ->
gen_server:call(?MODULE, {move_authenticator, ChainName, AuthenticatorID, Position}).
import_users(ChainName, AuthenticatorID, Filename) ->
gen_server:call(?MODULE, {import_users, ChainName, AuthenticatorID, Filename}).
add_user(ChainName, AuthenticatorID, UserInfo) ->
gen_server:call(?MODULE, {add_user, ChainName, AuthenticatorID, UserInfo}).
delete_user(ChainName, AuthenticatorID, UserID) ->
gen_server:call(?MODULE, {delete_user, ChainName, AuthenticatorID, UserID}).
update_user(ChainName, AuthenticatorID, UserID, NewUserInfo) ->
gen_server:call(?MODULE, {update_user, ChainName, AuthenticatorID, UserID, NewUserInfo}).
lookup_user(ChainName, AuthenticatorID, UserID) ->
gen_server:call(?MODULE, {lookup_user, ChainName, AuthenticatorID, UserID}).
%% TODO: Support pagination
list_users(ChainName, AuthenticatorID) ->
gen_server:call(?MODULE, {list_users, ChainName, AuthenticatorID}).
generate_id(#{mechanism := Mechanism0, backend := Backend0}) ->
Mechanism = atom_to_binary(Mechanism0),
Backend = atom_to_binary(Backend0),
<<Mechanism/binary, ":", Backend/binary>>;
generate_id(#{mechanism := Mechanism}) ->
atom_to_binary(Mechanism);
generate_id(#{<<"mechanism">> := Mechanism, <<"backend">> := Backend}) ->
<<Mechanism/binary, ":", Backend/binary>>;
generate_id(#{<<"mechanism">> := Mechanism}) ->
Mechanism.
%%--------------------------------------------------------------------
%% gen_server callbacks
%%--------------------------------------------------------------------
init(_Opts) ->
_ = ets:new(?CHAINS_TAB, [ named_table, set, public
, {keypos, #chain.name}
, {read_concurrency, true}]),
ok = emqx_config_handler:add_handler([authentication], ?MODULE),
ok = emqx_config_handler:add_handler([listeners, '?', '?', authentication], ?MODULE),
{ok, #{hooked => false, providers => #{}}}.
handle_call({add_provider, AuthNType, Provider}, _From, #{providers := Providers} = State) ->
reply(ok, State#{providers := Providers#{AuthNType => Provider}});
handle_call({remove_provider, AuthNType}, _From, #{providers := Providers} = State) ->
reply(ok, State#{providers := maps:remove(AuthNType, Providers)});
handle_call(get_refs, _From, #{providers := Providers} = State) ->
Refs = lists:foldl(fun({_, Provider}, Acc) ->
Acc ++ Provider:refs()
end, [], maps:to_list(Providers)),
reply({ok, Refs}, State);
handle_call({create_chain, Name}, _From, State) ->
case ets:member(?CHAINS_TAB, Name) of
true ->
reply({error, {already_exists, {chain, Name}}}, State);
false ->
Chain = #chain{name = Name,
authenticators = []},
true = ets:insert(?CHAINS_TAB, Chain),
reply({ok, serialize_chain(Chain)}, State)
end;
handle_call({delete_chain, Name}, _From, State) ->
case ets:lookup(?CHAINS_TAB, Name) of
[] ->
reply({error, {not_found, {chain, Name}}}, State);
[#chain{authenticators = Authenticators}] ->
_ = [do_delete_authenticator(Authenticator) || Authenticator <- Authenticators],
true = ets:delete(?CHAINS_TAB, Name),
reply(ok, may_unhook(State))
end;
handle_call({lookup_chain, Name}, _From, State) ->
case ets:lookup(?CHAINS_TAB, Name) of
[] ->
reply({error, {not_found, {chain, Name}}}, State);
[Chain] ->
reply({ok, serialize_chain(Chain)}, State)
end;
handle_call({create_authenticator, ChainName, Config}, _From, #{providers := Providers} = State) ->
UpdateFun =
fun(#chain{authenticators = Authenticators} = Chain) ->
AuthenticatorID = generate_id(Config),
case lists:keymember(AuthenticatorID, #authenticator.id, Authenticators) of
true ->
{error, {already_exists, {authenticator, AuthenticatorID}}};
false ->
case do_create_authenticator(ChainName, AuthenticatorID, Config, Providers) of
{ok, Authenticator} ->
NAuthenticators = Authenticators ++ [Authenticator],
true = ets:insert(?CHAINS_TAB, Chain#chain{authenticators = NAuthenticators}),
{ok, serialize_authenticator(Authenticator)};
{error, Reason} ->
{error, Reason}
end
end
end,
Reply = update_chain(ChainName, UpdateFun),
reply(Reply, may_hook(State));
handle_call({delete_authenticator, ChainName, AuthenticatorID}, _From, State) ->
UpdateFun =
fun(#chain{authenticators = Authenticators} = Chain) ->
case lists:keytake(AuthenticatorID, #authenticator.id, Authenticators) of
false ->
{error, {not_found, {authenticator, AuthenticatorID}}};
{value, Authenticator, NAuthenticators} ->
_ = do_delete_authenticator(Authenticator),
true = ets:insert(?CHAINS_TAB, Chain#chain{authenticators = NAuthenticators}),
ok
end
end,
Reply = update_chain(ChainName, UpdateFun),
reply(Reply, may_unhook(State));
handle_call({update_authenticator, ChainName, AuthenticatorID, Config}, _From, State) ->
UpdateFun =
fun(#chain{authenticators = Authenticators} = Chain) ->
case lists:keyfind(AuthenticatorID, #authenticator.id, Authenticators) of
false ->
{error, {not_found, {authenticator, AuthenticatorID}}};
#authenticator{provider = Provider,
state = #{version := Version} = ST} = Authenticator ->
case AuthenticatorID =:= generate_id(Config) of
true ->
Unique = unique(ChainName, AuthenticatorID, Version),
case Provider:update(Config#{'_unique' => Unique}, ST) of
{ok, NewST} ->
NewAuthenticator = Authenticator#authenticator{state = switch_version(NewST)},
NewAuthenticators = replace_authenticator(AuthenticatorID, NewAuthenticator, Authenticators),
true = ets:insert(?CHAINS_TAB, Chain#chain{authenticators = NewAuthenticators}),
{ok, serialize_authenticator(NewAuthenticator)};
{error, Reason} ->
{error, Reason}
end;
false ->
{error, mechanism_or_backend_change_is_not_alloed}
end
end
end,
Reply = update_chain(ChainName, UpdateFun),
reply(Reply, State);
handle_call({move_authenticator, ChainName, AuthenticatorID, Position}, _From, State) ->
UpdateFun =
fun(#chain{authenticators = Authenticators} = Chain) ->
case do_move_authenticator(AuthenticatorID, Authenticators, Position) of
{ok, NAuthenticators} ->
true = ets:insert(?CHAINS_TAB, Chain#chain{authenticators = NAuthenticators}),
ok;
{error, Reason} ->
{error, Reason}
end
end,
Reply = update_chain(ChainName, UpdateFun),
reply(Reply, State);
handle_call({import_users, ChainName, AuthenticatorID, Filename}, _From, State) ->
Reply = call_authenticator(ChainName, AuthenticatorID, import_users, [Filename]),
reply(Reply, State);
handle_call({add_user, ChainName, AuthenticatorID, UserInfo}, _From, State) ->
Reply = call_authenticator(ChainName, AuthenticatorID, add_user, [UserInfo]),
reply(Reply, State);
handle_call({delete_user, ChainName, AuthenticatorID, UserID}, _From, State) ->
Reply = call_authenticator(ChainName, AuthenticatorID, delete_user, [UserID]),
reply(Reply, State);
handle_call({update_user, ChainName, AuthenticatorID, UserID, NewUserInfo}, _From, State) ->
Reply = call_authenticator(ChainName, AuthenticatorID, update_user, [UserID, NewUserInfo]),
reply(Reply, State);
handle_call({lookup_user, ChainName, AuthenticatorID, UserID}, _From, State) ->
Reply = call_authenticator(ChainName, AuthenticatorID, lookup_user, [UserID]),
reply(Reply, State);
handle_call({list_users, ChainName, AuthenticatorID}, _From, State) ->
Reply = call_authenticator(ChainName, AuthenticatorID, list_users, []),
reply(Reply, State);
handle_call(Req, _From, State) ->
?LOG(error, "Unexpected call: ~p", [Req]),
{reply, ignored, State}.
handle_cast(Req, State) ->
?LOG(error, "Unexpected case: ~p", [Req]),
{noreply, State}.
handle_info(Info, State) ->
?LOG(error, "Unexpected info: ~p", [Info]),
{noreply, State}.
terminate(_Reason, _State) ->
emqx_config_handler:remove_handler([authentication]),
emqx_config_handler:remove_handler([listeners, '?', '?', authentication]),
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
reply(Reply, State) ->
{reply, Reply, State}.
%%------------------------------------------------------------------------------
%% Internal functions
%%------------------------------------------------------------------------------
split_by_id(ID, AuthenticatorsConfig) ->
case lists:foldl(
fun(C, {P1, P2, F0}) ->
F = case ID =:= generate_id(C) of
true -> true;
false -> F0
end,
case F of
false -> {[C | P1], P2, F};
true -> {P1, [C | P2], F}
end
end, {[], [], false}, AuthenticatorsConfig) of
{_, _, false} ->
{error, {not_found, {authenticator, ID}}};
{Part1, Part2, true} ->
{ok, lists:reverse(Part1), lists:reverse(Part2)}
end.
global_chain(mqtt) ->
'mqtt:global';
global_chain('mqtt-sn') ->
'mqtt-sn:global';
global_chain(coap) ->
'coap:global';
global_chain(lwm2m) ->
'lwm2m:global';
global_chain(stomp) ->
'stomp:global';
global_chain(_) ->
'unknown:global'.
may_hook(#{hooked := false} = State) ->
case lists:any(fun(#chain{authenticators = []}) -> false;
(_) -> true
end, ets:tab2list(?CHAINS_TAB)) of
true ->
_ = emqx:hook('client.authenticate', {emqx_authentication, authenticate, []}),
State#{hooked => true};
false ->
State
end;
may_hook(State) ->
State.
may_unhook(#{hooked := true} = State) ->
case lists:all(fun(#chain{authenticators = []}) -> true;
(_) -> false
end, ets:tab2list(?CHAINS_TAB)) of
true ->
_ = emqx:unhook('client.authenticate', {emqx_authentication, authenticate, []}),
State#{hooked => false};
false ->
State
end;
may_unhook(State) ->
State.
do_create_authenticator(ChainName, AuthenticatorID, #{enable := Enable} = Config, Providers) ->
case maps:get(authn_type(Config), Providers, undefined) of
undefined ->
{error, no_available_provider};
Provider ->
Unique = unique(ChainName, AuthenticatorID, ?VER_1),
case Provider:create(Config#{'_unique' => Unique}) of
{ok, State} ->
Authenticator = #authenticator{id = AuthenticatorID,
provider = Provider,
enable = Enable,
state = switch_version(State)},
{ok, Authenticator};
{error, Reason} ->
{error, Reason}
end
end.
do_delete_authenticator(#authenticator{provider = Provider, state = State}) ->
_ = Provider:destroy(State),
ok.
replace_authenticator(ID, Authenticator, Authenticators) ->
lists:keyreplace(ID, #authenticator.id, Authenticators, Authenticator).
do_move_authenticator(ID, Authenticators, Position) ->
case lists:keytake(ID, #authenticator.id, Authenticators) of
false ->
{error, {not_found, {authenticator, ID}}};
{value, Authenticator, NAuthenticators} ->
case Position of
top ->
{ok, [Authenticator | NAuthenticators]};
bottom ->
{ok, NAuthenticators ++ [Authenticator]};
{before, ID0} ->
insert(Authenticator, NAuthenticators, ID0, [])
end
end.
insert(_, [], ID, _) ->
{error, {not_found, {authenticator, ID}}};
insert(Authenticator, [#authenticator{id = ID} | _] = Authenticators, ID, Acc) ->
{ok, lists:reverse(Acc) ++ [Authenticator | Authenticators]};
insert(Authenticator, [Authenticator0 | More], ID, Acc) ->
insert(Authenticator, More, ID, [Authenticator0 | Acc]).
update_chain(ChainName, UpdateFun) ->
case ets:lookup(?CHAINS_TAB, ChainName) of
[] ->
{error, {not_found, {chain, ChainName}}};
[Chain] ->
UpdateFun(Chain)
end.
call_authenticator(ChainName, AuthenticatorID, Func, Args) ->
UpdateFun =
fun(#chain{authenticators = Authenticators}) ->
case lists:keyfind(AuthenticatorID, #authenticator.id, Authenticators) of
false ->
{error, {not_found, {authenticator, AuthenticatorID}}};
#authenticator{provider = Provider, state = State} ->
case erlang:function_exported(Provider, Func, length(Args) + 1) of
true ->
erlang:apply(Provider, Func, Args ++ [State]);
false ->
{error, unsupported_feature}
end
end
end,
update_chain(ChainName, UpdateFun).
serialize_chain(#chain{name = Name,
authenticators = Authenticators}) ->
#{ name => Name
, authenticators => serialize_authenticators(Authenticators)
}.
serialize_authenticators(Authenticators) ->
[serialize_authenticator(Authenticator) || Authenticator <- Authenticators].
serialize_authenticator(#authenticator{id = ID,
provider = Provider,
enable = Enable,
state = State}) ->
#{ id => ID
, provider => Provider
, enable => Enable
, state => State
}.
unique(ChainName, AuthenticatorID, Version) ->
NChainName = atom_to_binary(ChainName),
<<NChainName/binary, "/", AuthenticatorID/binary, ":", Version/binary>>.
switch_version(State = #{version := ?VER_1}) ->
State#{version := ?VER_2};
switch_version(State = #{version := ?VER_2}) ->
State#{version := ?VER_1};
switch_version(State) ->
State#{version => ?VER_1}.
authn_type(#{mechanism := Mechanism, backend := Backend}) ->
{Mechanism, Backend};
authn_type(#{mechanism := Mechanism}) ->
Mechanism.
may_to_map([L]) ->
L;
may_to_map(L) ->
L.
to_list(undefined) ->
[];
to_list(M) when M =:= #{} ->
[];
to_list(M) when is_map(M) ->
[M];
to_list(L) when is_list(L) ->
L.

View File

@ -37,6 +37,7 @@
, delete/1
, info/1
, format/1
, parse/1
]).
%% gen_server callbacks
@ -107,6 +108,33 @@ format(#banned{who = Who0,
until => to_rfc3339(Until)
}.
parse(Params) ->
Who = pares_who(Params),
By = maps:get(<<"by">>, Params, <<"mgmt_api">>),
Reason = maps:get(<<"reason">>, Params, <<"">>),
At = pares_time(maps:get(<<"at">>, Params, undefined), erlang:system_time(second)),
Until = pares_time(maps:get(<<"until">>, Params, undefined), At + 5 * 60),
#banned{
who = Who,
by = By,
reason = Reason,
at = At,
until = Until
}.
pares_who(#{as := As, who := Who}) ->
pares_who(#{<<"as">> => As, <<"who">> => Who});
pares_who(#{<<"as">> := <<"peerhost">>, <<"who">> := Peerhost0}) ->
{ok, Peerhost} = inet:parse_address(binary_to_list(Peerhost0)),
{peerhost, Peerhost};
pares_who(#{<<"as">> := As, <<"who">> := Who}) ->
{binary_to_atom(As, utf8), Who}.
pares_time(undefined, Default) ->
Default;
pares_time(Rfc3339, _Default) ->
to_timestamp(Rfc3339).
maybe_format_host({peerhost, Host}) ->
AddrBinary = list_to_binary(inet:ntoa(Host)),
{peerhost, AddrBinary};
@ -116,6 +144,11 @@ maybe_format_host({As, Who}) ->
to_rfc3339(Timestamp) ->
list_to_binary(calendar:system_time_to_rfc3339(Timestamp, [{unit, second}])).
to_timestamp(Rfc3339) when is_binary(Rfc3339) ->
to_timestamp(binary_to_list(Rfc3339));
to_timestamp(Rfc3339) ->
calendar:rfc3339_to_system_time(Rfc3339, [{unit, second}]).
-spec(create(emqx_types:banned() | map()) -> ok).
create(#{who := Who,
by := By,
@ -130,12 +163,16 @@ create(#{who := Who,
create(Banned) when is_record(Banned, banned) ->
ekka_mnesia:dirty_write(?BANNED_TAB, Banned).
look_up(Who) when is_map(Who) ->
look_up(pares_who(Who));
look_up(Who) ->
mnesia:dirty_read(?BANNED_TAB, Who).
-spec(delete({clientid, emqx_types:clientid()}
| {username, emqx_types:username()}
| {peerhost, emqx_types:peerhost()}) -> ok).
delete(Who) when is_map(Who)->
delete(pares_who(Who));
delete(Who) ->
ekka_mnesia:dirty_delete(?BANNED_TAB, Who).

View File

@ -43,6 +43,14 @@ init([]) ->
type => worker,
modules => [emqx_shared_sub]},
%% Authentication
AuthN = #{id => authn,
start => {emqx_authentication, start_link, []},
restart => permanent,
shutdown => 2000,
type => worker,
modules => [emqx_authentication]},
%% Broker helper
Helper = #{id => helper,
start => {emqx_broker_helper, start_link, []},
@ -51,5 +59,5 @@ init([]) ->
type => worker,
modules => [emqx_broker_helper]},
{ok, {{one_for_all, 0, 1}, [BrokerPool, SharedSub, Helper]}}.
{ok, {{one_for_all, 0, 1}, [BrokerPool, SharedSub, AuthN, Helper]}}.

View File

@ -214,7 +214,7 @@ init(ConnInfo = #{peername := {PeerHost, _Port},
ClientInfo = set_peercert_infos(
Peercert,
#{zone => Zone,
listener => Listener,
listener => emqx_listeners:listener_id(Type, Listener),
protocol => Protocol,
peerhost => PeerHost,
sockport => SockPort,
@ -223,7 +223,7 @@ init(ConnInfo = #{peername := {PeerHost, _Port},
mountpoint => MountPoint,
is_bridge => false,
is_superuser => false
}, Zone, Listener),
}, Zone),
{NClientInfo, NConnInfo} = take_ws_cookie(ClientInfo, ConnInfo),
#channel{conninfo = NConnInfo,
clientinfo = NClientInfo,
@ -244,12 +244,12 @@ quota_policy(RawPolicy) ->
erlang:trunc(hocon_postprocess:duration(StrWind) / 1000)}}
|| {Name, [StrCount, StrWind]} <- maps:to_list(RawPolicy)].
set_peercert_infos(NoSSL, ClientInfo, _, _)
set_peercert_infos(NoSSL, ClientInfo, _)
when NoSSL =:= nossl;
NoSSL =:= undefined ->
ClientInfo#{username => undefined};
set_peercert_infos(Peercert, ClientInfo, Zone, _Listener) ->
set_peercert_infos(Peercert, ClientInfo, Zone) ->
{DN, CN} = {esockd_peercert:subject(Peercert),
esockd_peercert:common_name(Peercert)},
PeercetAs = fun(Key) ->
@ -1303,11 +1303,11 @@ do_authenticate(#{auth_method := AuthMethod} = Credential, #channel{clientinfo =
case emqx_access_control:authenticate(Credential) of
{ok, Result} ->
{ok, Properties,
Channel#channel{clientinfo = ClientInfo#{is_superuser => maps:get(superuser, Result, false)},
Channel#channel{clientinfo = ClientInfo#{is_superuser => maps:get(is_superuser, Result, false)},
auth_cache = #{}}};
{ok, Result, AuthData} ->
{ok, Properties#{'Authentication-Data' => AuthData},
Channel#channel{clientinfo = ClientInfo#{is_superuser => maps:get(superuser, Result, false)},
Channel#channel{clientinfo = ClientInfo#{is_superuser => maps:get(is_superuser, Result, false)},
auth_cache = #{}}};
{continue, AuthCache} ->
{continue, Properties, Channel#channel{auth_cache = AuthCache}};
@ -1320,8 +1320,8 @@ do_authenticate(#{auth_method := AuthMethod} = Credential, #channel{clientinfo =
do_authenticate(Credential, #channel{clientinfo = ClientInfo} = Channel) ->
case emqx_access_control:authenticate(Credential) of
{ok, #{superuser := Superuser}} ->
{ok, #{}, Channel#channel{clientinfo = ClientInfo#{is_superuser => Superuser}}};
{ok, #{is_superuser := IsSuperuser}} ->
{ok, #{}, Channel#channel{clientinfo = ClientInfo#{is_superuser => IsSuperuser}}};
{error, Reason} ->
{error, emqx_reason_codes:connack_error(Reason)}
end.

View File

@ -22,49 +22,38 @@
-export([init/1]).
%%--------------------------------------------------------------------
%% API
%%--------------------------------------------------------------------
start_link() ->
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
%%--------------------------------------------------------------------
%% Supervisor callbacks
%%--------------------------------------------------------------------
init([]) ->
Banned = #{id => banned,
start => {emqx_banned, start_link, []},
restart => permanent,
shutdown => 1000,
type => worker,
modules => [emqx_banned]},
Flapping = #{id => flapping,
start => {emqx_flapping, start_link, []},
restart => permanent,
shutdown => 1000,
type => worker,
modules => [emqx_flapping]},
%% Channel locker
Locker = #{id => locker,
start => {emqx_cm_locker, start_link, []},
restart => permanent,
shutdown => 5000,
type => worker,
modules => [emqx_cm_locker]
},
%% Channel registry
Registry = #{id => registry,
start => {emqx_cm_registry, start_link, []},
restart => permanent,
shutdown => 5000,
type => worker,
modules => [emqx_cm_registry]
},
%% Channel Manager
Manager = #{id => manager,
start => {emqx_cm, start_link, []},
restart => permanent,
shutdown => 5000,
type => worker,
modules => [emqx_cm]
},
SupFlags = #{strategy => one_for_one,
intensity => 100,
period => 10
},
Banned = child_spec(emqx_banned, 1000, worker),
Flapping = child_spec(emqx_flapping, 1000, worker),
Locker = child_spec(emqx_cm_locker, 5000, worker),
Registry = child_spec(emqx_cm_registry, 5000, worker),
Manager = child_spec(emqx_cm, 5000, worker),
{ok, {SupFlags, [Banned, Flapping, Locker, Registry, Manager]}}.
%%--------------------------------------------------------------------
%% Internal functions
%%--------------------------------------------------------------------
child_spec(Mod, Shutdown, Type) ->
#{id => Mod,
start => {Mod, start_link, []},
restart => permanent,
shutdown => Shutdown,
type => Type,
modules => [Mod]
}.

View File

@ -87,15 +87,21 @@
-type update_request() :: term().
-type update_cmd() :: {update, update_request()} | remove.
-type update_opts() :: #{
%% fill the default values into the rawconf map
rawconf_with_defaults => boolean()
%% rawconf_with_defaults:
%% fill the default values into the `raw_config` field of the return value
%% defaults to `false`
rawconf_with_defaults => boolean(),
%% persistent:
%% save the updated config to the emqx_override.conf file
%% defaults to `true`
persistent => boolean()
}.
-type update_args() :: {update_cmd(), Opts :: update_opts()}.
-type update_stage() :: pre_config_update | post_config_update.
-type update_error() :: {update_stage(), module(), term()} | {save_configs, term()} | term().
-type update_result() :: #{
config := emqx_config:config(),
raw_config := emqx_config:raw_config(),
config => emqx_config:config(),
raw_config => emqx_config:raw_config(),
post_config_update => #{module() => any()}
}.
@ -235,7 +241,7 @@ put_raw(KeyPath, Config) -> do_put(?RAW_CONF, KeyPath, Config).
%% in the rear of the list overrides prior values.
-spec init_load(module(), [string()] | binary() | hocon:config()) -> ok.
init_load(SchemaMod, Conf) when is_list(Conf) orelse is_binary(Conf) ->
ParseOptions = #{format => richmap},
ParseOptions = #{format => map},
Parser = case is_binary(Conf) of
true -> fun hocon:binary/2;
false -> fun hocon:files/2
@ -249,19 +255,17 @@ init_load(SchemaMod, Conf) when is_list(Conf) orelse is_binary(Conf) ->
}),
error(failed_to_load_hocon_conf)
end;
init_load(SchemaMod, RawRichConf) when is_map(RawRichConf) ->
%% check with richmap for line numbers in error reports (future enhancement)
Opts = #{return_plain => true,
nullable => true
},
%% this call throws exception in case of check failure
{_AppEnvs, CheckedConf} = hocon_schema:map_translate(SchemaMod, RawRichConf, Opts),
init_load(SchemaMod, RawConf0) when is_map(RawConf0) ->
ok = save_schema_mod_and_names(SchemaMod),
ok = save_to_config_map(emqx_map_lib:unsafe_atom_key_map(normalize_conf(CheckedConf)),
normalize_conf(hocon_schema:richmap_to_map(RawRichConf))).
%% override part of the input conf using emqx_override.conf
RawConf = merge_with_override_conf(RawConf0),
%% check and save configs
{_AppEnvs, CheckedConf} = check_config(SchemaMod, RawConf),
ok = save_to_config_map(maps:with(get_atom_root_names(), CheckedConf),
maps:with(get_root_names(), RawConf)).
normalize_conf(Conf) ->
maps:with(get_root_names(bin), Conf).
merge_with_override_conf(RawConf) ->
maps:merge(RawConf, maps:with(maps:keys(RawConf), read_override_conf())).
-spec check_config(module(), raw_config()) -> {AppEnvs, CheckedConf}
when AppEnvs :: app_envs(), CheckedConf :: config().
@ -277,7 +281,7 @@ check_config(SchemaMod, RawConf) ->
-spec fill_defaults(raw_config()) -> map().
fill_defaults(RawConf) ->
RootNames = get_root_names(bin),
RootNames = get_root_names(),
maps:fold(fun(Key, Conf, Acc) ->
SubMap = #{Key => Conf},
WithDefaults = case lists:member(Key, RootNames) of
@ -320,8 +324,8 @@ get_schema_mod(RootName) ->
get_root_names() ->
maps:get(names, persistent_term:get(?PERSIS_SCHEMA_MODS, #{names => []})).
get_root_names(bin) ->
maps:keys(get_schema_mod()).
get_atom_root_names() ->
[atom(N) || N <- get_root_names()].
-spec save_configs(app_envs(), config(), raw_config(), raw_config()) -> ok | {error, term()}.
save_configs(_AppEnvs, Conf, RawConf, OverrideConf) ->
@ -344,14 +348,19 @@ save_to_config_map(Conf, RawConf) ->
?MODULE:put_raw(RawConf).
-spec save_to_override_conf(raw_config()) -> ok | {error, term()}.
save_to_override_conf(undefined) ->
ok;
save_to_override_conf(RawConf) ->
FileName = emqx_override_conf_name(),
case emqx_override_conf_name() of
undefined -> ok;
FileName ->
ok = filelib:ensure_dir(FileName),
case file:write_file(FileName, jsx:prettify(jsx:encode(RawConf))) of
ok -> ok;
{error, Reason} ->
logger:error("write to ~s failed, ~p", [FileName, Reason]),
{error, Reason}
end
end.
load_hocon_file(FileName, LoadType) ->
@ -363,7 +372,7 @@ load_hocon_file(FileName, LoadType) ->
end.
emqx_override_conf_name() ->
application:get_env(emqx, override_conf_file, "emqx_override.conf").
application:get_env(emqx, override_conf_file, undefined).
do_get(Type, KeyPath) ->
Ref = make_ref(),
@ -412,14 +421,7 @@ do_deep_put(?RAW_CONF, KeyPath, Map, Value) ->
root_names_from_conf(RawConf) ->
Keys = maps:keys(RawConf),
StrNames = [str(K) || K <- Keys],
AtomNames = lists:foldl(fun(K, Acc) ->
try [atom(K) | Acc]
catch error:badarg -> Acc
end
end, [], Keys),
PossibleNames = StrNames ++ AtomNames,
[Name || Name <- get_root_names(), lists:member(Name, PossibleNames)].
[Name || Name <- get_root_names(), lists:member(Name, Keys)].
atom(Bin) when is_binary(Bin) ->
binary_to_existing_atom(Bin, latin1);
@ -428,13 +430,6 @@ atom(Str) when is_list(Str) ->
atom(Atom) when is_atom(Atom) ->
Atom.
str(Bin) when is_binary(Bin) ->
binary_to_list(Bin);
str(Str) when is_list(Str) ->
Str;
str(Atom) when is_atom(Atom) ->
atom_to_list(Atom).
bin(Bin) when is_binary(Bin) -> Bin;
bin(Str) when is_list(Str) -> list_to_binary(Str);
bin(Atom) when is_atom(Atom) -> atom_to_binary(Atom, utf8).

View File

@ -23,6 +23,7 @@
%% API functions
-export([ start_link/0
, stop/0
, add_handler/2
, remove_handler/1
, update_config/3
@ -38,6 +39,7 @@
code_change/3]).
-define(MOD, {mod}).
-define(WKEY, '?').
-define(ATOM_CONF_PATH(PATH, EXP, EXP_ON_FAIL),
try [safe_atom(Key) || Key <- PATH] of
@ -68,6 +70,9 @@
start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, {}, []).
stop() ->
gen_server:stop(?MODULE).
-spec update_config(module(), emqx_config:config_key_path(), emqx_config:update_args()) ->
{ok, emqx_config:update_result()} | {error, emqx_config:update_error()}.
update_config(SchemaModule, ConfKeyPath, UpdateArgs) ->
@ -76,11 +81,11 @@ update_config(SchemaModule, ConfKeyPath, UpdateArgs) ->
-spec add_handler(emqx_config:config_key_path(), handler_name()) -> ok.
add_handler(ConfKeyPath, HandlerName) ->
gen_server:call(?MODULE, {add_child, ConfKeyPath, HandlerName}).
gen_server:call(?MODULE, {add_handler, ConfKeyPath, HandlerName}).
-spec remove_handler(emqx_config:config_key_path()) -> ok.
remove_handler(ConfKeyPath) ->
gen_server:call(?MODULE, {remove_child, ConfKeyPath}).
gen_server:call(?MODULE, {remove_handler, ConfKeyPath}).
%%============================================================================
@ -88,15 +93,18 @@ remove_handler(ConfKeyPath) ->
init(_) ->
{ok, #{handlers => #{?MOD => ?MODULE}}}.
handle_call({add_child, ConfKeyPath, HandlerName}, _From,
State = #{handlers := Handlers}) ->
{reply, ok, State#{handlers =>
emqx_map_lib:deep_put(ConfKeyPath, Handlers, #{?MOD => HandlerName})}};
handle_call({add_handler, ConfKeyPath, HandlerName}, _From, State = #{handlers := Handlers}) ->
case deep_put_handler(ConfKeyPath, Handlers, HandlerName) of
{ok, NewHandlers} ->
{reply, ok, State#{handlers => NewHandlers}};
Error ->
{reply, Error, State}
end;
handle_call({remove_child, ConfKeyPath}, _From,
handle_call({remove_handler, ConfKeyPath}, _From,
State = #{handlers := Handlers}) ->
{reply, ok, State#{handlers =>
emqx_map_lib:deep_remove(ConfKeyPath, Handlers)}};
emqx_map_lib:deep_remove(ConfKeyPath ++ [?MOD], Handlers)}};
handle_call({change_config, SchemaModule, ConfKeyPath, UpdateArgs}, _From,
#{handlers := Handlers} = State) ->
@ -130,17 +138,40 @@ terminate(_Reason, _State) ->
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
process_update_request(ConfKeyPath, _Handlers, {remove, _Opts}) ->
deep_put_handler([], Handlers, Mod) when is_map(Handlers) ->
{ok, Handlers#{?MOD => Mod}};
deep_put_handler([], _Handlers, Mod) ->
{ok, #{?MOD => Mod}};
deep_put_handler([?WKEY | KeyPath], Handlers, Mod) ->
deep_put_handler2(?WKEY, KeyPath, Handlers, Mod);
deep_put_handler([Key | KeyPath], Handlers, Mod) ->
case maps:find(?WKEY, Handlers) of
error ->
deep_put_handler2(Key, KeyPath, Handlers, Mod);
{ok, _SubHandlers} ->
{error, {cannot_override_a_wildcard_path, [?WKEY | KeyPath]}}
end.
deep_put_handler2(Key, KeyPath, Handlers, Mod) ->
SubHandlers = maps:get(Key, Handlers, #{}),
case deep_put_handler(KeyPath, SubHandlers, Mod) of
{ok, SubHandlers1} ->
{ok, Handlers#{Key => SubHandlers1}};
Error ->
Error
end.
process_update_request(ConfKeyPath, _Handlers, {remove, Opts}) ->
OldRawConf = emqx_config:get_root_raw(ConfKeyPath),
BinKeyPath = bin_path(ConfKeyPath),
NewRawConf = emqx_map_lib:deep_remove(BinKeyPath, OldRawConf),
OverrideConf = emqx_map_lib:deep_remove(BinKeyPath, emqx_config:read_override_conf()),
OverrideConf = remove_from_override_config(BinKeyPath, Opts),
{ok, NewRawConf, OverrideConf};
process_update_request(ConfKeyPath, Handlers, {{update, UpdateReq}, _Opts}) ->
process_update_request(ConfKeyPath, Handlers, {{update, UpdateReq}, Opts}) ->
OldRawConf = emqx_config:get_root_raw(ConfKeyPath),
case do_update_config(ConfKeyPath, Handlers, OldRawConf, UpdateReq) of
{ok, NewRawConf} ->
OverrideConf = update_override_config(NewRawConf),
OverrideConf = update_override_config(NewRawConf, Opts),
{ok, NewRawConf, OverrideConf};
Error -> Error
end.
@ -149,7 +180,7 @@ do_update_config([], Handlers, OldRawConf, UpdateReq) ->
call_pre_config_update(Handlers, OldRawConf, UpdateReq);
do_update_config([ConfKey | ConfKeyPath], Handlers, OldRawConf, UpdateReq) ->
SubOldRawConf = get_sub_config(bin(ConfKey), OldRawConf),
SubHandlers = maps:get(ConfKey, Handlers, #{}),
SubHandlers = get_sub_handlers(ConfKey, Handlers),
case do_update_config(ConfKeyPath, SubHandlers, SubOldRawConf, UpdateReq) of
{ok, NewUpdateReq} ->
call_pre_config_update(Handlers, OldRawConf, #{bin(ConfKey) => NewUpdateReq});
@ -180,7 +211,7 @@ do_post_config_update([ConfKey | ConfKeyPath], Handlers, OldConf, NewConf, AppEn
Result) ->
SubOldConf = get_sub_config(ConfKey, OldConf),
SubNewConf = get_sub_config(ConfKey, NewConf),
SubHandlers = maps:get(ConfKey, Handlers, #{}),
SubHandlers = get_sub_handlers(ConfKey, Handlers),
case do_post_config_update(ConfKeyPath, SubHandlers, SubOldConf, SubNewConf, AppEnvs,
UpdateArgs, Result) of
{ok, Result1} ->
@ -189,6 +220,12 @@ do_post_config_update([ConfKey | ConfKeyPath], Handlers, OldConf, NewConf, AppEn
Error -> Error
end.
get_sub_handlers(ConfKey, Handlers) ->
case maps:find(ConfKey, Handlers) of
error -> maps:get(?WKEY, Handlers, #{});
{ok, SubHandlers} -> SubHandlers
end.
get_sub_config(ConfKey, Conf) when is_map(Conf) ->
maps:get(ConfKey, Conf, undefined);
get_sub_config(_, _Conf) -> %% the Conf is a primitive
@ -217,10 +254,9 @@ call_post_config_update(Handlers, OldConf, NewConf, AppEnvs, UpdateReq, Result)
false -> {ok, Result}
end.
save_configs(ConfKeyPath, AppEnvs, CheckedConf, NewRawConf, OverrideConf, {_Cmd, Opts}) ->
save_configs(ConfKeyPath, AppEnvs, CheckedConf, NewRawConf, OverrideConf, UpdateArgs) ->
case emqx_config:save_configs(AppEnvs, CheckedConf, NewRawConf, OverrideConf) of
ok -> {ok, #{config => emqx_config:get(ConfKeyPath),
raw_config => return_rawconf(ConfKeyPath, Opts)}};
ok -> {ok, return_change_result(ConfKeyPath, UpdateArgs)};
{error, Reason} -> {error, {save_configs, Reason}}
end.
@ -234,13 +270,27 @@ merge_to_old_config(UpdateReq, RawConf) when is_map(UpdateReq), is_map(RawConf)
merge_to_old_config(UpdateReq, _RawConf) ->
{ok, UpdateReq}.
update_override_config(RawConf) ->
remove_from_override_config(_BinKeyPath, #{persistent := false}) ->
undefined;
remove_from_override_config(BinKeyPath, _Opts) ->
OldConf = emqx_config:read_override_conf(),
emqx_map_lib:deep_remove(BinKeyPath, OldConf).
update_override_config(_RawConf, #{persistent := false}) ->
undefined;
update_override_config(RawConf, _Opts) ->
OldConf = emqx_config:read_override_conf(),
maps:merge(OldConf, RawConf).
up_req({remove, _Opts}) -> '$remove';
up_req({{update, Req}, _Opts}) -> Req.
return_change_result(ConfKeyPath, {{update, _Req}, Opts}) ->
#{config => emqx_config:get(ConfKeyPath),
raw_config => return_rawconf(ConfKeyPath, Opts)};
return_change_result(_ConfKeyPath, {remove, _Opts}) ->
#{}.
return_rawconf(ConfKeyPath, #{rawconf_with_defaults := true}) ->
FullRawConf = emqx_config:fill_defaults(emqx_config:get_raw([])),
emqx_map_lib:deep_get(bin_path(ConfKeyPath), FullRawConf);

View File

@ -100,14 +100,10 @@ parse(<<Type:4, Dup:1, QoS:2, Retain:1, Rest/binary>>,
StrictMode andalso validate_header(Type, Dup, QoS, Retain),
Header = #mqtt_packet_header{type = Type,
dup = bool(Dup),
qos = QoS,
qos = fixqos(Type, QoS),
retain = bool(Retain)
},
Header1 = case fixqos(Type, QoS) of
QoS -> Header;
FixedQoS -> Header#mqtt_packet_header{qos = FixedQoS}
end,
parse_remaining_len(Rest, Header1, Options);
parse_remaining_len(Rest, Header, Options);
parse(Bin, {{len, #{hdr := Header,
len := {Multiplier, Length}}

View File

@ -26,6 +26,8 @@
, restart/0
, stop/0
, is_running/1
, current_conns/2
, max_conns/2
]).
-export([ start_listener/1
@ -41,10 +43,14 @@
, parse_listener_id/1
]).
-export([post_config_update/4]).
-define(CONF_KEY_PATH, [listeners]).
%% @doc List configured listeners.
-spec(list() -> [{ListenerId :: atom(), ListenerConf :: map()}]).
list() ->
[{listener_id(ZoneName, LName), LConf} || {ZoneName, LName, LConf} <- do_list()].
[{listener_id(Type, LName), LConf} || {Type, LName, LConf} <- do_list()].
do_list() ->
Listeners = maps:to_list(emqx:get_config([listeners], #{})),
@ -58,7 +64,7 @@ list(Type, Conf) ->
-spec is_running(ListenerId :: atom()) -> boolean() | {error, no_found}.
is_running(ListenerId) ->
case lists:filtermap(fun({_Zone, Id, #{running := IsRunning}}) ->
case lists:filtermap(fun({_Type, Id, #{running := IsRunning}}) ->
Id =:= ListenerId andalso {true, IsRunning}
end, do_list()) of
[IsRunning] -> IsRunning;
@ -85,9 +91,34 @@ is_running(quic, _ListenerId, _Conf)->
%% TODO: quic support
{error, no_found}.
current_conns(ID, ListenOn) ->
{Type, Name} = parse_listener_id(ID),
current_conns(Type, Name, ListenOn).
current_conns(Type, Name, ListenOn) when Type == tcl; Type == ssl ->
esockd:get_current_connections({listener_id(Type, Name), ListenOn});
current_conns(Type, Name, _ListenOn) when Type =:= ws; Type =:= wss ->
proplists:get_value(all_connections, ranch:info(listener_id(Type, Name)));
current_conns(_, _, _) ->
{error, not_support}.
max_conns(ID, ListenOn) ->
{Type, Name} = parse_listener_id(ID),
max_conns(Type, Name, ListenOn).
max_conns(Type, Name, ListenOn) when Type == tcl; Type == ssl ->
esockd:get_max_connections({listener_id(Type, Name), ListenOn});
max_conns(Type, Name, _ListenOn) when Type =:= ws; Type =:= wss ->
proplists:get_value(max_connections, ranch:info(listener_id(Type, Name)));
max_conns(_, _, _) ->
{error, not_support}.
%% @doc Start all listeners.
-spec(start() -> ok).
start() ->
%% The ?MODULE:start/0 will be called by emqx_app when emqx get started,
%% so we install the config handler here.
ok = emqx_config_handler:add_handler(?CONF_KEY_PATH, ?MODULE),
foreach_listeners(fun start_listener/3).
-spec start_listener(atom()) -> ok | {error, term()}.
@ -102,7 +133,7 @@ start_listener(Type, ListenerName, #{bind := Bind} = Conf) ->
console_print("- Skip - starting listener ~s on ~s ~n due to ~p",
[listener_id(Type, ListenerName), format_addr(Bind), Reason]);
{ok, _} ->
console_print("Start listener ~s on ~s successfully.~n",
console_print("Listener ~s on ~s started.~n",
[listener_id(Type, ListenerName), format_addr(Bind)]);
{error, {already_started, Pid}} ->
{error, {already_started, Pid}};
@ -122,27 +153,47 @@ restart_listener(ListenerId) ->
apply_on_listener(ListenerId, fun restart_listener/3).
-spec(restart_listener(atom(), atom(), map()) -> ok | {error, term()}).
restart_listener(Type, ListenerName, {OldConf, NewConf}) ->
restart_listener(Type, ListenerName, OldConf, NewConf);
restart_listener(Type, ListenerName, Conf) ->
case stop_listener(Type, ListenerName, Conf) of
ok -> start_listener(Type, ListenerName, Conf);
restart_listener(Type, ListenerName, Conf, Conf).
restart_listener(Type, ListenerName, OldConf, NewConf) ->
case stop_listener(Type, ListenerName, OldConf) of
ok -> start_listener(Type, ListenerName, NewConf);
Error -> Error
end.
%% @doc Stop all listeners.
-spec(stop() -> ok).
stop() ->
%% The ?MODULE:stop/0 will be called by emqx_app when emqx is going to shutdown,
%% so we uninstall the config handler here.
_ = emqx_config_handler:remove_handler(?CONF_KEY_PATH),
foreach_listeners(fun stop_listener/3).
-spec(stop_listener(atom()) -> ok | {error, term()}).
stop_listener(ListenerId) ->
apply_on_listener(ListenerId, fun stop_listener/3).
-spec(stop_listener(atom(), atom(), map()) -> ok | {error, term()}).
stop_listener(Type, ListenerName, #{bind := ListenOn}) when Type == tcp; Type == ssl ->
stop_listener(Type, ListenerName, #{bind := Bind} = Conf) ->
case do_stop_listener(Type, ListenerName, Conf) of
ok ->
console_print("Listener ~s on ~s stopped.~n",
[listener_id(Type, ListenerName), format_addr(Bind)]),
ok;
{error, Reason} ->
?ELOG("Failed to stop listener ~s on ~s: ~0p~n",
[listener_id(Type, ListenerName), format_addr(Bind), Reason]),
{error, Reason}
end.
-spec(do_stop_listener(atom(), atom(), map()) -> ok | {error, term()}).
do_stop_listener(Type, ListenerName, #{bind := ListenOn}) when Type == tcp; Type == ssl ->
esockd:close(listener_id(Type, ListenerName), ListenOn);
stop_listener(Type, ListenerName, _Conf) when Type == ws; Type == wss ->
do_stop_listener(Type, ListenerName, _Conf) when Type == ws; Type == wss ->
cowboy:stop_listener(listener_id(Type, ListenerName));
stop_listener(quic, ListenerName, _Conf) ->
do_stop_listener(quic, ListenerName, _Conf) ->
quicer:stop_listener(listener_id(quic, ListenerName)).
-ifndef(TEST).
@ -201,6 +252,36 @@ do_start_listener(quic, ListenerName, #{bind := ListenOn} = Opts) ->
{ok, {skipped, quic_app_missing}}
end.
delete_authentication(Type, ListenerName, _Conf) ->
emqx_authentication:delete_chain(atom_to_binary(listener_id(Type, ListenerName))).
%% Update the listeners at runtime
post_config_update(_Req, NewListeners, OldListeners, _AppEnvs) ->
#{added := Added, removed := Removed, changed := Updated}
= diff_listeners(NewListeners, OldListeners),
perform_listener_changes(fun stop_listener/3, Removed),
perform_listener_changes(fun delete_authentication/3, Removed),
perform_listener_changes(fun start_listener/3, Added),
perform_listener_changes(fun restart_listener/3, Updated).
perform_listener_changes(Action, MapConfs) ->
lists:foreach(fun
({Id, Conf}) ->
{Type, Name} = parse_listener_id(Id),
Action(Type, Name, Conf)
end, maps:to_list(MapConfs)).
diff_listeners(NewListeners, OldListeners) ->
emqx_map_lib:diff_maps(flatten_listeners(NewListeners), flatten_listeners(OldListeners)).
flatten_listeners(Conf0) ->
maps:from_list(
lists:append([do_flatten_listeners(Type, Conf)
|| {Type, Conf} <- maps:to_list(Conf0)])).
do_flatten_listeners(Type, Conf0) ->
[{listener_id(Type, Name), maps:remove(authentication, Conf)} || {Name, Conf} <- maps:to_list(Conf0)].
esockd_opts(Type, Opts0) ->
Opts1 = maps:with([acceptors, max_connections, proxy_protocol, proxy_protocol_timeout], Opts0),
Opts2 = case emqx_config:get_zone_conf(zone(Opts0), [rate_limit, max_conn_rate]) of
@ -265,12 +346,12 @@ format_addr({Addr, Port}) when is_tuple(Addr) ->
io_lib:format("~s:~w", [inet:ntoa(Addr), Port]).
listener_id(Type, ListenerName) ->
list_to_atom(lists:append([atom_to_list(Type), ":", atom_to_list(ListenerName)])).
list_to_atom(lists:append([str(Type), ":", str(ListenerName)])).
parse_listener_id(Id) ->
try
[Zone, Listen] = string:split(atom_to_list(Id), ":", leading),
{list_to_existing_atom(Zone), list_to_existing_atom(Listen)}
[Type, Name] = string:split(str(Id), ":", leading),
{list_to_existing_atom(Type), list_to_atom(Name)}
catch
_ : _ -> error({invalid_listener_id, Id})
end.
@ -291,8 +372,8 @@ tcp_opts(Opts) ->
foreach_listeners(Do) ->
lists:foreach(
fun({ZoneName, LName, LConf}) ->
Do(ZoneName, LName, LConf)
fun({Type, LName, LConf}) ->
Do(Type, LName, LConf)
end, do_list()).
has_enabled_listener_conf_by_type(Type) ->
@ -307,3 +388,10 @@ apply_on_listener(ListenerId, Do) ->
{not_found, _, _} -> error({listener_config_not_found, Type, ListenerName});
{ok, Conf} -> Do(Type, ListenerName, Conf)
end.
str(A) when is_atom(A) ->
atom_to_list(A);
str(B) when is_binary(B) ->
binary_to_list(B);
str(S) when is_list(S) ->
S.

View File

@ -24,13 +24,16 @@
, safe_atom_key_map/1
, unsafe_atom_key_map/1
, jsonable_map/1
, jsonable_value/1
, deep_convert/2
, jsonable_map/2
, binary_string/1
, deep_convert/3
, diff_maps/2
]).
-export_type([config_key/0, config_key_path/0]).
-type config_key() :: atom() | binary().
-type config_key_path() :: [config_key()].
-type convert_fun() :: fun((...) -> {K1::any(), V1::any()} | drop).
%%-----------------------------------------------------------------
-spec deep_get(config_key_path(), map()) -> term().
@ -62,13 +65,11 @@ deep_find(_KeyPath, Data) ->
{not_found, _KeyPath, Data}.
-spec deep_put(config_key_path(), map(), term()) -> map().
deep_put([], Map, Data) when is_map(Map) ->
Data;
deep_put([], _Map, Data) -> %% not map, replace it
deep_put([], _Map, Data) ->
Data;
deep_put([Key | KeyPath], Map, Data) ->
SubMap = deep_put(KeyPath, maps:get(Key, Map, #{}), Data),
Map#{Key => SubMap}.
SubMap = maps:get(Key, Map, #{}),
Map#{Key => deep_put(KeyPath, SubMap, Data)}.
-spec deep_remove(config_key_path(), map()) -> map().
deep_remove([], Map) ->
@ -100,15 +101,17 @@ deep_merge(BaseMap, NewMap) ->
end, #{}, BaseMap),
maps:merge(MergedBase, maps:with(NewKeys, NewMap)).
-spec deep_convert(map(), fun((K::any(), V::any()) -> {K1::any(), V1::any()})) -> map().
deep_convert(Map, ConvFun) when is_map(Map) ->
-spec deep_convert(map(), convert_fun(), Args::list()) -> map().
deep_convert(Map, ConvFun, Args) when is_map(Map) ->
maps:fold(fun(K, V, Acc) ->
{K1, V1} = ConvFun(K, deep_convert(V, ConvFun)),
Acc#{K1 => V1}
case apply(ConvFun, [K, deep_convert(V, ConvFun, Args) | Args]) of
drop -> Acc;
{K1, V1} -> Acc#{K1 => V1}
end
end, #{}, Map);
deep_convert(ListV, ConvFun) when is_list(ListV) ->
[deep_convert(V, ConvFun) || V <- ListV];
deep_convert(Val, _) -> Val.
deep_convert(ListV, ConvFun, Args) when is_list(ListV) ->
[deep_convert(V, ConvFun, Args) || V <- ListV];
deep_convert(Val, _, _Args) -> Val.
-spec unsafe_atom_key_map(#{binary() | atom() => any()}) -> #{atom() => any()}.
unsafe_atom_key_map(Map) ->
@ -120,17 +123,45 @@ safe_atom_key_map(Map) ->
-spec jsonable_map(map() | list()) -> map() | list().
jsonable_map(Map) ->
deep_convert(Map, fun(K, V) ->
{jsonable_value(K), jsonable_value(V)}
end).
jsonable_map(Map, fun(K, V) -> {K, V} end).
jsonable_value([]) -> [];
jsonable_value(Val) when is_list(Val) ->
jsonable_map(Map, JsonableFun) ->
deep_convert(Map, fun binary_string_kv/3, [JsonableFun]).
-spec diff_maps(map(), map()) ->
#{added := map(), identical := map(), removed := map(),
changed := #{any() => {OldValue::any(), NewValue::any()}}}.
diff_maps(NewMap, OldMap) ->
InitR = #{identical => #{}, changed => #{}, removed => #{}},
{Result, RemInNew} =
lists:foldl(fun({OldK, OldV}, {Result0 = #{identical := I, changed := U, removed := D},
RemNewMap}) ->
Result1 = case maps:find(OldK, NewMap) of
error ->
Result0#{removed => D#{OldK => OldV}};
{ok, NewV} when NewV == OldV ->
Result0#{identical => I#{OldK => OldV}};
{ok, NewV} ->
Result0#{changed => U#{OldK => {OldV, NewV}}}
end,
{Result1, maps:remove(OldK, RemNewMap)}
end, {InitR, NewMap}, maps:to_list(OldMap)),
Result#{added => RemInNew}.
binary_string_kv(K, V, JsonableFun) ->
case JsonableFun(K, V) of
drop -> drop;
{K1, V1} -> {binary_string(K1), binary_string(V1)}
end.
binary_string([]) -> [];
binary_string(Val) when is_list(Val) ->
case io_lib:printable_unicode_list(Val) of
true -> unicode:characters_to_binary(Val);
false -> Val
false -> [binary_string(V) || V <- Val]
end;
jsonable_value(Val) ->
binary_string(Val) ->
Val.
%%---------------------------------------------------------------------------
@ -138,4 +169,4 @@ covert_keys_to_atom(BinKeyMap, Conv) ->
deep_convert(BinKeyMap, fun
(K, V) when is_atom(K) -> {K, V};
(K, V) when is_binary(K) -> {Conv(K), V}
end).
end, []).

View File

@ -22,8 +22,6 @@
-include("logger.hrl").
-include("types.hrl").
-include("emqx_mqtt.hrl").
-include("emqx.hrl").
-export([ start_link/0
, stop/0

File diff suppressed because it is too large Load Diff

View File

@ -13,13 +13,22 @@
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
%%
-module(emqx_gateway_api_client).
-behaviour(minirest_api).
-module(emqx_zone_schema).
%% minirest behaviour callbacks
-export([api_spec/0]).
-export([namespace/0, roots/0, fields/1]).
api_spec() ->
{[], []}.
namespace() -> zone.
roots() -> [].
%% zone schemas are clones from the same name from root level
%% only not allowed to have default values.
fields(Name) ->
[{N, no_default(Sc)} || {N, Sc} <- emqx_schema:fields(Name)].
%% no default values for zone settings
no_default(Sc) ->
fun(default) -> undefined;
(Other) -> hocon_schema:field_schema(Sc, Other)
end.

View File

@ -0,0 +1,238 @@
%%--------------------------------------------------------------------
%% 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_authentication_SUITE).
-behaviour(hocon_schema).
-behaviour(emqx_authentication).
-compile(export_all).
-compile(nowarn_export_all).
-include_lib("common_test/include/ct.hrl").
-include_lib("eunit/include/eunit.hrl").
-include_lib("typerefl/include/types.hrl").
-export([ fields/1 ]).
-export([ refs/0
, create/1
, update/2
, authenticate/2
, destroy/1
]).
-define(AUTHN, emqx_authentication).
%%------------------------------------------------------------------------------
%% Hocon Schema
%%------------------------------------------------------------------------------
fields(type1) ->
[ {mechanism, {enum, ['password-based']}}
, {backend, {enum, ['built-in-database']}}
, {enable, fun enable/1}
];
fields(type2) ->
[ {mechanism, {enum, ['password-based']}}
, {backend, {enum, ['mysql']}}
, {enable, fun enable/1}
].
enable(type) -> boolean();
enable(default) -> true;
enable(_) -> undefined.
%%------------------------------------------------------------------------------
%% Callbacks
%%------------------------------------------------------------------------------
refs() ->
[ hoconsc:ref(?MODULE, type1)
, hoconsc:ref(?MODULE, type2)
].
create(_Config) ->
{ok, #{mark => 1}}.
update(_Config, _State) ->
{ok, #{mark => 2}}.
authenticate(#{username := <<"good">>}, _State) ->
{ok, #{is_superuser => true}};
authenticate(#{username := _}, _State) ->
{error, bad_username_or_password}.
destroy(_State) ->
ok.
all() ->
emqx_ct:all(?MODULE).
init_per_suite(Config) ->
application:set_env(ekka, strict_mode, true),
emqx_ct_helpers:start_apps([]),
Config.
end_per_suite(_) ->
emqx_ct_helpers:stop_apps([]),
ok.
t_chain(_) ->
% CRUD of authentication chain
ChainName = 'test',
?assertMatch({ok, []}, ?AUTHN:list_chains()),
?assertMatch({ok, #{name := ChainName, authenticators := []}}, ?AUTHN:create_chain(ChainName)),
?assertEqual({error, {already_exists, {chain, ChainName}}}, ?AUTHN:create_chain(ChainName)),
?assertMatch({ok, #{name := ChainName, authenticators := []}}, ?AUTHN:lookup_chain(ChainName)),
?assertMatch({ok, [#{name := ChainName}]}, ?AUTHN:list_chains()),
?assertEqual(ok, ?AUTHN:delete_chain(ChainName)),
?assertMatch({error, {not_found, {chain, ChainName}}}, ?AUTHN:lookup_chain(ChainName)),
ok.
t_authenticator(_) ->
ChainName = 'test',
AuthenticatorConfig1 = #{mechanism => 'password-based',
backend => 'built-in-database',
enable => true},
% Create an authenticator when the authentication chain does not exist
?assertEqual({error, {not_found, {chain, ChainName}}}, ?AUTHN:create_authenticator(ChainName, AuthenticatorConfig1)),
?AUTHN:create_chain(ChainName),
% Create an authenticator when the provider does not exist
?assertEqual({error, no_available_provider}, ?AUTHN:create_authenticator(ChainName, AuthenticatorConfig1)),
AuthNType1 = {'password-based', 'built-in-database'},
?AUTHN:add_provider(AuthNType1, ?MODULE),
ID1 = <<"password-based:built-in-database">>,
% CRUD of authencaticator
?assertMatch({ok, #{id := ID1, state := #{mark := 1}}}, ?AUTHN:create_authenticator(ChainName, AuthenticatorConfig1)),
?assertMatch({ok, #{id := ID1}}, ?AUTHN:lookup_authenticator(ChainName, ID1)),
?assertMatch({ok, [#{id := ID1}]}, ?AUTHN:list_authenticators(ChainName)),
?assertEqual({error, {already_exists, {authenticator, ID1}}}, ?AUTHN:create_authenticator(ChainName, AuthenticatorConfig1)),
?assertMatch({ok, #{id := ID1, state := #{mark := 2}}}, ?AUTHN:update_authenticator(ChainName, ID1, AuthenticatorConfig1)),
?assertEqual(ok, ?AUTHN:delete_authenticator(ChainName, ID1)),
?assertEqual({error, {not_found, {authenticator, ID1}}}, ?AUTHN:update_authenticator(ChainName, ID1, AuthenticatorConfig1)),
?assertMatch({ok, []}, ?AUTHN:list_authenticators(ChainName)),
% Multiple authenticators exist at the same time
AuthNType2 = {'password-based', mysql},
?AUTHN:add_provider(AuthNType2, ?MODULE),
ID2 = <<"password-based:mysql">>,
AuthenticatorConfig2 = #{mechanism => 'password-based',
backend => mysql,
enable => true},
?assertMatch({ok, #{id := ID1}}, ?AUTHN:create_authenticator(ChainName, AuthenticatorConfig1)),
?assertMatch({ok, #{id := ID2}}, ?AUTHN:create_authenticator(ChainName, AuthenticatorConfig2)),
% Move authenticator
?assertMatch({ok, [#{id := ID1}, #{id := ID2}]}, ?AUTHN:list_authenticators(ChainName)),
?assertEqual(ok, ?AUTHN:move_authenticator(ChainName, ID2, top)),
?assertMatch({ok, [#{id := ID2}, #{id := ID1}]}, ?AUTHN:list_authenticators(ChainName)),
?assertEqual(ok, ?AUTHN:move_authenticator(ChainName, ID2, bottom)),
?assertMatch({ok, [#{id := ID1}, #{id := ID2}]}, ?AUTHN:list_authenticators(ChainName)),
?assertEqual(ok, ?AUTHN:move_authenticator(ChainName, ID2, {before, ID1})),
?assertMatch({ok, [#{id := ID2}, #{id := ID1}]}, ?AUTHN:list_authenticators(ChainName)),
?AUTHN:delete_chain(ChainName),
?AUTHN:remove_provider(AuthNType1),
?AUTHN:remove_provider(AuthNType2),
ok.
t_authenticate(_) ->
ListenerID = 'tcp:default',
ClientInfo = #{zone => default,
listener => ListenerID,
protocol => mqtt,
username => <<"good">>,
password => <<"any">>},
?assertEqual({ok, #{is_superuser => false}}, emqx_access_control:authenticate(ClientInfo)),
AuthNType = {'password-based', 'built-in-database'},
?AUTHN:add_provider(AuthNType, ?MODULE),
AuthenticatorConfig = #{mechanism => 'password-based',
backend => 'built-in-database',
enable => true},
?AUTHN:create_chain(ListenerID),
?assertMatch({ok, _}, ?AUTHN:create_authenticator(ListenerID, AuthenticatorConfig)),
?assertEqual({ok, #{is_superuser => true}}, emqx_access_control:authenticate(ClientInfo)),
?assertEqual({error, bad_username_or_password}, emqx_access_control:authenticate(ClientInfo#{username => <<"bad">>})),
?AUTHN:delete_chain(ListenerID),
?AUTHN:remove_provider(AuthNType),
ok.
t_update_config(_) ->
emqx_config_handler:add_handler([authentication], emqx_authentication),
AuthNType1 = {'password-based', 'built-in-database'},
AuthNType2 = {'password-based', mysql},
?AUTHN:add_provider(AuthNType1, ?MODULE),
?AUTHN:add_provider(AuthNType2, ?MODULE),
Global = 'mqtt:global',
AuthenticatorConfig1 = #{mechanism => 'password-based',
backend => 'built-in-database',
enable => true},
AuthenticatorConfig2 = #{mechanism => 'password-based',
backend => mysql,
enable => true},
ID1 = <<"password-based:built-in-database">>,
ID2 = <<"password-based:mysql">>,
?assertMatch({ok, []}, ?AUTHN:list_chains()),
?assertMatch({ok, _}, update_config([authentication], {create_authenticator, Global, AuthenticatorConfig1})),
?assertMatch({ok, #{id := ID1, state := #{mark := 1}}}, ?AUTHN:lookup_authenticator(Global, ID1)),
?assertMatch({ok, _}, update_config([authentication], {create_authenticator, Global, AuthenticatorConfig2})),
?assertMatch({ok, #{id := ID2, state := #{mark := 1}}}, ?AUTHN:lookup_authenticator(Global, ID2)),
?assertMatch({ok, _}, update_config([authentication], {update_authenticator, Global, ID1, #{}})),
?assertMatch({ok, #{id := ID1, state := #{mark := 2}}}, ?AUTHN:lookup_authenticator(Global, ID1)),
?assertMatch({ok, _}, update_config([authentication], {move_authenticator, Global, ID2, <<"top">>})),
?assertMatch({ok, [#{id := ID2}, #{id := ID1}]}, ?AUTHN:list_authenticators(Global)),
?assertMatch({ok, _}, update_config([authentication], {delete_authenticator, Global, ID1})),
?assertEqual({error, {not_found, {authenticator, ID1}}}, ?AUTHN:lookup_authenticator(Global, ID1)),
ListenerID = 'tcp:default',
ConfKeyPath = [listeners, tcp, default, authentication],
?assertMatch({ok, _}, update_config(ConfKeyPath, {create_authenticator, ListenerID, AuthenticatorConfig1})),
?assertMatch({ok, #{id := ID1, state := #{mark := 1}}}, ?AUTHN:lookup_authenticator(ListenerID, ID1)),
?assertMatch({ok, _}, update_config(ConfKeyPath, {create_authenticator, ListenerID, AuthenticatorConfig2})),
?assertMatch({ok, #{id := ID2, state := #{mark := 1}}}, ?AUTHN:lookup_authenticator(ListenerID, ID2)),
?assertMatch({ok, _}, update_config(ConfKeyPath, {update_authenticator, ListenerID, ID1, #{}})),
?assertMatch({ok, #{id := ID1, state := #{mark := 2}}}, ?AUTHN:lookup_authenticator(ListenerID, ID1)),
?assertMatch({ok, _}, update_config(ConfKeyPath, {move_authenticator, ListenerID, ID2, <<"top">>})),
?assertMatch({ok, [#{id := ID2}, #{id := ID1}]}, ?AUTHN:list_authenticators(ListenerID)),
?assertMatch({ok, _}, update_config(ConfKeyPath, {delete_authenticator, ListenerID, ID1})),
?assertEqual({error, {not_found, {authenticator, ID1}}}, ?AUTHN:lookup_authenticator(ListenerID, ID1)),
?AUTHN:delete_chain(Global),
?AUTHN:remove_provider(AuthNType1),
?AUTHN:remove_provider(AuthNType2),
ok.
update_config(Path, ConfigRequest) ->
emqx:update_config(Path, ConfigRequest, #{rawconf_with_defaults => true}).

View File

@ -144,7 +144,7 @@ init_per_suite(Config) ->
%% Access Control Meck
ok = meck:new(emqx_access_control, [passthrough, no_history, no_link]),
ok = meck:expect(emqx_access_control, authenticate,
fun(_) -> {ok, #{superuser => false}} end),
fun(_) -> {ok, #{is_superuser => false}} end),
ok = meck:expect(emqx_access_control, authorize, fun(_, _, _) -> allow end),
%% Broker Meck
ok = meck:new(emqx_broker, [passthrough, no_history, no_link]),

View File

@ -114,8 +114,8 @@ t_cm(_) ->
emqx_config:put_zone_conf(default, [mqtt, idle_timeout], 15000).
t_cm_registry(_) ->
Info = supervisor:which_children(emqx_cm_sup),
{_, Pid, _, _} = lists:keyfind(registry, 1, Info),
Children = supervisor:which_children(emqx_cm_sup),
{_, Pid, _, _} = lists:keyfind(emqx_cm_registry, 1, Children),
ignored = gen_server:call(Pid, <<"Unexpected call">>),
gen_server:cast(Pid, <<"Unexpected cast">>),
Pid ! <<"Unexpected info">>.

View File

@ -0,0 +1,50 @@
%%--------------------------------------------------------------------
%% 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_config_SUITE).
-compile(export_all).
-compile(nowarn_export_all).
-include_lib("eunit/include/eunit.hrl").
all() -> emqx_ct:all(?MODULE).
init_per_suite(Config) ->
emqx_ct_helpers:boot_modules(all),
emqx_ct_helpers:start_apps([]),
Config.
end_per_suite(_Config) ->
emqx_ct_helpers:stop_apps([]).
t_fill_default_values(_) ->
Conf = #{
<<"broker">> => #{
<<"perf">> => #{},
<<"route_batch_clean">> => false}
},
?assertMatch(#{<<"broker">> :=
#{<<"enable_session_registry">> := true,
<<"perf">> :=
#{<<"route_lock_type">> := key,
<<"trie_compaction">> := true},
<<"route_batch_clean">> := false,
<<"session_locking_strategy">> := quorum,
<<"shared_dispatch_ack_enabled">> := false,
<<"shared_subscription_strategy">> := round_robin,
<<"sys_heartbeat_interval">> := "30s",
<<"sys_msg_interval">> := "1m"}},
emqx_config:fill_defaults(Conf)).

View File

@ -55,8 +55,8 @@ t_detect_check(_) ->
true = emqx_banned:check(ClientInfo),
timer:sleep(3000),
false = emqx_banned:check(ClientInfo),
Childrens = supervisor:which_children(emqx_cm_sup),
{flapping, Pid, _, _} = lists:keyfind(flapping, 1, Childrens),
Children = supervisor:which_children(emqx_cm_sup),
{emqx_flapping, Pid, _, _} = lists:keyfind(emqx_flapping, 1, Children),
gen_server:call(Pid, unexpected_msg),
gen_server:cast(Pid, unexpected_msg),
Pid ! test,

View File

@ -37,6 +37,14 @@ end_per_suite(_Config) ->
application:stop(esockd),
application:stop(cowboy).
init_per_testcase(_, Config) ->
{ok, _} = emqx_config_handler:start_link(),
Config.
end_per_testcase(_, _Config) ->
_ = emqx_config_handler:stop(),
ok.
t_start_stop_listeners(_) ->
ok = emqx_listeners:start(),
?assertException(error, _, emqx_listeners:start_listener({ws,{"127.0.0.1", 8083}, []})),

View File

@ -1,3 +1,3 @@
user_id,password_hash,salt,superuser
user_id,password_hash,salt,is_superuser
myuser3,b6c743545a7817ae8c8f624371d5f5f0373234bb0ff36b8ffbf19bce0e06ab75,de1024f462fb83910fd13151bd4bd235,true
myuser4,ee68c985a69208b6eda8c6c9b4c7c2d2b15ee2352cdd64a903171710a99182e8,ad773b5be9dd0613fe6c2f4d8c403139,false

1 user_id password_hash salt superuser is_superuser
2 myuser3 b6c743545a7817ae8c8f624371d5f5f0373234bb0ff36b8ffbf19bce0e06ab75 de1024f462fb83910fd13151bd4bd235 true true
3 myuser4 ee68c985a69208b6eda8c6c9b4c7c2d2b15ee2352cdd64a903171710a99182e8 ad773b5be9dd0613fe6c2f4d8c403139 false false

View File

@ -3,12 +3,12 @@
"user_id":"myuser1",
"password_hash":"c5e46903df45e5dc096dc74657610dbee8deaacae656df88a1788f1847390242",
"salt": "e378187547bf2d6f0545a3f441aa4d8a",
"superuser": true
"is_superuser": true
},
{
"user_id":"myuser2",
"password_hash":"f4d17f300b11e522fd33f497c11b126ef1ea5149c74d2220f9a16dc876d4567b",
"salt": "6d3f9bd5b54d94b98adbcfe10b6d181f",
"superuser": false
"is_superuser": false
}
]

View File

@ -1,37 +1,6 @@
authentication {
enable = false
authenticators = [
# {
# name: "authenticator1"
# mechanism: password-based
# server_type: built-in-database
# user_id_type: clientid
# },
# {
# name: "authenticator2"
# mechanism: password-based
# server_type: mongodb
# server: "127.0.0.1:27017"
# database: mqtt
# collection: users
# selector: {
# username: "${mqtt-username}"
# }
# password_hash_field: password_hash
# salt_field: salt
# password_hash_algorithm: sha256
# salt_position: prefix
# },
# {
# name: "authenticator 3"
# mechanism: password-based
# server_type: redis
# server: "127.0.0.1:6379"
# password: "public"
# database: 0
# query: "HMGET ${mqtt-username} password_hash salt"
# password_hash_algorithm: sha256
# salt_position: prefix
# }
]
}
# authentication: {
# mechanism: password-based
# backend: built-in-database
# user_id_type: clientid
# }

View File

@ -15,24 +15,11 @@
%%--------------------------------------------------------------------
-define(APP, emqx_authn).
-define(CHAIN, <<"mqtt">>).
-define(VER_1, <<"1">>).
-define(VER_2, <<"2">>).
-define(AUTHN, emqx_authentication).
-define(GLOBAL, 'mqtt:global').
-define(RE_PLACEHOLDER, "\\$\\{[a-z0-9\\-]+\\}").
-record(authenticator,
{ id :: binary()
, name :: binary()
, provider :: module()
, state :: map()
}).
-record(chain,
{ id :: binary()
, authenticators :: [{binary(), binary(), #authenticator{}}]
, created_at :: integer()
}).
-define(AUTH_SHARD, emqx_authn_shard).

View File

@ -1,6 +1,4 @@
{deps, [
{jose, {git, "https://github.com/potatosalad/erlang-jose", {tag, "1.11.1"}}}
]}.
{deps, []}.
{edoc_opts, [{preprocess, true}]}.
{erl_opts, [warn_unused_vars,

View File

@ -15,640 +15,3 @@
%%--------------------------------------------------------------------
-module(emqx_authn).
-behaviour(gen_server).
-behaviour(emqx_config_handler).
-include("emqx_authn.hrl").
-include_lib("emqx/include/logger.hrl").
-export([ pre_config_update/2
, post_config_update/4
, update_config/2
]).
-export([ enable/0
, disable/0
, is_enabled/0
]).
-export([authenticate/2]).
-export([ start_link/0
, stop/0
]).
-export([ create_chain/1
, delete_chain/1
, lookup_chain/1
, list_chains/0
, create_authenticator/2
, delete_authenticator/2
, update_authenticator/3
, update_or_create_authenticator/3
, lookup_authenticator/2
, list_authenticators/1
, move_authenticator/3
]).
-export([ import_users/3
, add_user/3
, delete_user/3
, update_user/4
, lookup_user/3
, list_users/2
]).
%% gen_server callbacks
-export([ init/1
, handle_call/3
, handle_cast/2
, handle_info/2
, terminate/2
, code_change/3
]).
-define(CHAIN_TAB, emqx_authn_chain).
%%------------------------------------------------------------------------------
%% APIs
%%------------------------------------------------------------------------------
pre_config_update({enable, Enable}, _OldConfig) ->
{ok, Enable};
pre_config_update({create_authenticator, Config}, OldConfig) ->
{ok, OldConfig ++ [Config]};
pre_config_update({delete_authenticator, ID}, OldConfig) ->
case lookup_authenticator(?CHAIN, ID) of
{error, Reason} -> {error, Reason};
{ok, #{name := Name}} ->
NewConfig = lists:filter(fun(#{<<"name">> := N}) ->
N =/= Name
end, OldConfig),
{ok, NewConfig}
end;
pre_config_update({update_authenticator, ID, Config}, OldConfig) ->
case lookup_authenticator(?CHAIN, ID) of
{error, Reason} -> {error, Reason};
{ok, #{name := Name}} ->
NewConfig = lists:map(fun(#{<<"name">> := N} = C) ->
case N =:= Name of
true -> Config;
false -> C
end
end, OldConfig),
{ok, NewConfig}
end;
pre_config_update({update_or_create_authenticator, ID, Config}, OldConfig) ->
case lookup_authenticator(?CHAIN, ID) of
{error, _Reason} -> OldConfig ++ [Config];
{ok, #{name := Name}} ->
NewConfig = lists:map(fun(#{<<"name">> := N} = C) ->
case N =:= Name of
true -> Config;
false -> C
end
end, OldConfig),
{ok, NewConfig}
end;
pre_config_update({move_authenticator, ID, Position}, OldConfig) ->
case lookup_authenticator(?CHAIN, ID) of
{error, Reason} -> {error, Reason};
{ok, #{name := Name}} ->
{ok, Found, Part1, Part2} = split_by_name(Name, OldConfig),
case Position of
<<"top">> ->
{ok, [Found | Part1] ++ Part2};
<<"bottom">> ->
{ok, Part1 ++ Part2 ++ [Found]};
Before ->
case binary:split(Before, <<":">>, [global]) of
[<<"before">>, ID0] ->
case lookup_authenticator(?CHAIN, ID0) of
{error, Reason} -> {error, Reason};
{ok, #{name := Name1}} ->
{ok, NFound, NPart1, NPart2} = split_by_name(Name1, Part1 ++ Part2),
{ok, NPart1 ++ [Found, NFound | NPart2]}
end;
_ ->
{error, {invalid_parameter, position}}
end
end
end.
post_config_update({enable, true}, _NewConfig, _OldConfig, _AppEnvs) ->
emqx_authn:enable();
post_config_update({enable, false}, _NewConfig, _OldConfig, _AppEnvs) ->
emqx_authn:disable();
post_config_update({create_authenticator, #{<<"name">> := Name}}, NewConfig, _OldConfig, _AppEnvs) ->
case lists:filter(
fun(#{name := N}) ->
N =:= Name
end, NewConfig) of
[Config] ->
create_authenticator(?CHAIN, Config);
[_Config | _] ->
{error, name_has_be_used}
end;
post_config_update({delete_authenticator, ID}, _NewConfig, _OldConfig, _AppEnvs) ->
case delete_authenticator(?CHAIN, ID) of
ok -> ok;
{error, Reason} -> throw(Reason)
end;
post_config_update({update_authenticator, ID, #{<<"name">> := Name}}, NewConfig, _OldConfig, _AppEnvs) ->
case lists:filter(
fun(#{name := N}) ->
N =:= Name
end, NewConfig) of
[Config] ->
update_authenticator(?CHAIN, ID, Config);
[_Config | _] ->
{error, name_has_be_used}
end;
post_config_update({update_or_create_authenticator, ID, #{<<"name">> := Name}}, NewConfig, _OldConfig, _AppEnvs) ->
case lists:filter(
fun(#{name := N}) ->
N =:= Name
end, NewConfig) of
[Config] ->
update_or_create_authenticator(?CHAIN, ID, Config);
[_Config | _] ->
{error, name_has_be_used}
end;
post_config_update({move_authenticator, ID, Position}, _NewConfig, _OldConfig, _AppEnvs) ->
NPosition = case Position of
<<"top">> -> top;
<<"bottom">> -> bottom;
Before ->
case binary:split(Before, <<":">>, [global]) of
[<<"before">>, ID0] ->
{before, ID0};
_ ->
{error, {invalid_parameter, position}}
end
end,
move_authenticator(?CHAIN, ID, NPosition).
update_config(Path, ConfigRequest) ->
emqx:update_config(Path, ConfigRequest, #{rawconf_with_defaults => true}).
enable() ->
case emqx:hook('client.authenticate', {?MODULE, authenticate, []}) of
ok -> ok;
{error, already_exists} -> ok
end.
disable() ->
emqx:unhook('client.authenticate', {?MODULE, authenticate, []}),
ok.
is_enabled() ->
Callbacks = emqx_hooks:lookup('client.authenticate'),
lists:any(fun({callback, {?MODULE, authenticate, []}, _, _}) ->
true;
(_) ->
false
end, Callbacks).
authenticate(Credential, _AuthResult) ->
case ets:lookup(?CHAIN_TAB, ?CHAIN) of
[#chain{authenticators = Authenticators}] ->
do_authenticate(Authenticators, Credential);
[] ->
{stop, {error, not_authorized}}
end.
do_authenticate([], _) ->
{stop, {error, not_authorized}};
do_authenticate([{_, _, #authenticator{provider = Provider, state = State}} | More], Credential) ->
case Provider:authenticate(Credential, State) of
ignore ->
do_authenticate(More, Credential);
Result ->
%% {ok, Extra}
%% {ok, Extra, AuthData}
%% {ok, MetaData}
%% {continue, AuthCache}
%% {continue, AuthData, AuthCache}
%% {error, Reason}
{stop, Result}
end.
start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
stop() ->
gen_server:stop(?MODULE).
create_chain(#{id := ID}) ->
gen_server:call(?MODULE, {create_chain, ID}).
delete_chain(ID) ->
gen_server:call(?MODULE, {delete_chain, ID}).
lookup_chain(ID) ->
gen_server:call(?MODULE, {lookup_chain, ID}).
list_chains() ->
Chains = ets:tab2list(?CHAIN_TAB),
{ok, [serialize_chain(Chain) || Chain <- Chains]}.
create_authenticator(ChainID, Config) ->
gen_server:call(?MODULE, {create_authenticator, ChainID, Config}).
delete_authenticator(ChainID, AuthenticatorID) ->
gen_server:call(?MODULE, {delete_authenticator, ChainID, AuthenticatorID}).
update_authenticator(ChainID, AuthenticatorID, Config) ->
gen_server:call(?MODULE, {update_authenticator, ChainID, AuthenticatorID, Config}).
update_or_create_authenticator(ChainID, AuthenticatorID, Config) ->
gen_server:call(?MODULE, {update_or_create_authenticator, ChainID, AuthenticatorID, Config}).
lookup_authenticator(ChainID, AuthenticatorID) ->
case ets:lookup(?CHAIN_TAB, ChainID) of
[] ->
{error, {not_found, {chain, ChainID}}};
[#chain{authenticators = Authenticators}] ->
case lists:keyfind(AuthenticatorID, 1, Authenticators) of
false ->
{error, {not_found, {authenticator, AuthenticatorID}}};
{_, _, Authenticator} ->
{ok, serialize_authenticator(Authenticator)}
end
end.
list_authenticators(ChainID) ->
case ets:lookup(?CHAIN_TAB, ChainID) of
[] ->
{error, {not_found, {chain, ChainID}}};
[#chain{authenticators = Authenticators}] ->
{ok, serialize_authenticators(Authenticators)}
end.
move_authenticator(ChainID, AuthenticatorID, Position) ->
gen_server:call(?MODULE, {move_authenticator, ChainID, AuthenticatorID, Position}).
import_users(ChainID, AuthenticatorID, Filename) ->
gen_server:call(?MODULE, {import_users, ChainID, AuthenticatorID, Filename}).
add_user(ChainID, AuthenticatorID, UserInfo) ->
gen_server:call(?MODULE, {add_user, ChainID, AuthenticatorID, UserInfo}).
delete_user(ChainID, AuthenticatorID, UserID) ->
gen_server:call(?MODULE, {delete_user, ChainID, AuthenticatorID, UserID}).
update_user(ChainID, AuthenticatorID, UserID, NewUserInfo) ->
gen_server:call(?MODULE, {update_user, ChainID, AuthenticatorID, UserID, NewUserInfo}).
lookup_user(ChainID, AuthenticatorID, UserID) ->
gen_server:call(?MODULE, {lookup_user, ChainID, AuthenticatorID, UserID}).
%% TODO: Support pagination
list_users(ChainID, AuthenticatorID) ->
gen_server:call(?MODULE, {list_users, ChainID, AuthenticatorID}).
%%--------------------------------------------------------------------
%% gen_server callbacks
%%--------------------------------------------------------------------
init(_Opts) ->
_ = ets:new(?CHAIN_TAB, [ named_table, set, public
, {keypos, #chain.id}
, {read_concurrency, true}]),
{ok, #{}}.
handle_call({create_chain, ID}, _From, State) ->
case ets:member(?CHAIN_TAB, ID) of
true ->
reply({error, {already_exists, {chain, ID}}}, State);
false ->
Chain = #chain{id = ID,
authenticators = [],
created_at = erlang:system_time(millisecond)},
true = ets:insert(?CHAIN_TAB, Chain),
reply({ok, serialize_chain(Chain)}, State)
end;
handle_call({delete_chain, ID}, _From, State) ->
case ets:lookup(?CHAIN_TAB, ID) of
[] ->
reply({error, {not_found, {chain, ID}}}, State);
[#chain{authenticators = Authenticators}] ->
_ = [do_delete_authenticator(Authenticator) || {_, _, Authenticator} <- Authenticators],
true = ets:delete(?CHAIN_TAB, ID),
reply(ok, State)
end;
handle_call({lookup_chain, ID}, _From, State) ->
case ets:lookup(?CHAIN_TAB, ID) of
[] ->
reply({error, {not_found, {chain, ID}}}, State);
[Chain] ->
reply({ok, serialize_chain(Chain)}, State)
end;
handle_call({create_authenticator, ChainID, #{name := Name} = Config}, _From, State) ->
UpdateFun =
fun(#chain{authenticators = Authenticators} = Chain) ->
case lists:keymember(Name, 2, Authenticators) of
true ->
{error, name_has_be_used};
false ->
AlreadyExist = fun(ID) ->
lists:keymember(ID, 1, Authenticators)
end,
AuthenticatorID = gen_id(AlreadyExist),
case do_create_authenticator(ChainID, AuthenticatorID, Config) of
{ok, Authenticator} ->
NAuthenticators = Authenticators ++ [{AuthenticatorID, Name, Authenticator}],
true = ets:insert(?CHAIN_TAB, Chain#chain{authenticators = NAuthenticators}),
{ok, serialize_authenticator(Authenticator)};
{error, Reason} ->
{error, Reason}
end
end
end,
Reply = update_chain(ChainID, UpdateFun),
reply(Reply, State);
handle_call({delete_authenticator, ChainID, AuthenticatorID}, _From, State) ->
UpdateFun =
fun(#chain{authenticators = Authenticators} = Chain) ->
case lists:keytake(AuthenticatorID, 1, Authenticators) of
false ->
{error, {not_found, {authenticator, AuthenticatorID}}};
{value, {_, _, Authenticator}, NAuthenticators} ->
_ = do_delete_authenticator(Authenticator),
true = ets:insert(?CHAIN_TAB, Chain#chain{authenticators = NAuthenticators}),
ok
end
end,
Reply = update_chain(ChainID, UpdateFun),
reply(Reply, State);
handle_call({update_authenticator, ChainID, AuthenticatorID, Config}, _From, State) ->
Reply = update_or_create_authenticator(ChainID, AuthenticatorID, Config, false),
reply(Reply, State);
handle_call({update_or_create_authenticator, ChainID, AuthenticatorID, Config}, _From, State) ->
Reply = update_or_create_authenticator(ChainID, AuthenticatorID, Config, true),
reply(Reply, State);
handle_call({move_authenticator, ChainID, AuthenticatorID, Position}, _From, State) ->
UpdateFun =
fun(#chain{authenticators = Authenticators} = Chain) ->
case do_move_authenticator(AuthenticatorID, Authenticators, Position) of
{ok, NAuthenticators} ->
true = ets:insert(?CHAIN_TAB, Chain#chain{authenticators = NAuthenticators}),
ok;
{error, Reason} ->
{error, Reason}
end
end,
Reply = update_chain(ChainID, UpdateFun),
reply(Reply, State);
handle_call({import_users, ChainID, AuthenticatorID, Filename}, _From, State) ->
Reply = call_authenticator(ChainID, AuthenticatorID, import_users, [Filename]),
reply(Reply, State);
handle_call({add_user, ChainID, AuthenticatorID, UserInfo}, _From, State) ->
Reply = call_authenticator(ChainID, AuthenticatorID, add_user, [UserInfo]),
reply(Reply, State);
handle_call({delete_user, ChainID, AuthenticatorID, UserID}, _From, State) ->
Reply = call_authenticator(ChainID, AuthenticatorID, delete_user, [UserID]),
reply(Reply, State);
handle_call({update_user, ChainID, AuthenticatorID, UserID, NewUserInfo}, _From, State) ->
Reply = call_authenticator(ChainID, AuthenticatorID, update_user, [UserID, NewUserInfo]),
reply(Reply, State);
handle_call({lookup_user, ChainID, AuthenticatorID, UserID}, _From, State) ->
Reply = call_authenticator(ChainID, AuthenticatorID, lookup_user, [UserID]),
reply(Reply, State);
handle_call({list_users, ChainID, AuthenticatorID}, _From, State) ->
Reply = call_authenticator(ChainID, AuthenticatorID, list_users, []),
reply(Reply, State);
handle_call(Req, _From, State) ->
?LOG(error, "Unexpected call: ~p", [Req]),
{reply, ignored, State}.
handle_cast(Req, State) ->
?LOG(error, "Unexpected case: ~p", [Req]),
{noreply, State}.
handle_info(Info, State) ->
?LOG(error, "Unexpected info: ~p", [Info]),
{noreply, State}.
terminate(_Reason, _State) ->
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
reply(Reply, State) ->
{reply, Reply, State}.
%%------------------------------------------------------------------------------
%% Internal functions
%%------------------------------------------------------------------------------
authenticator_provider(#{mechanism := 'password-based', server_type := 'built-in-database'}) ->
emqx_authn_mnesia;
authenticator_provider(#{mechanism := 'password-based', server_type := 'mysql'}) ->
emqx_authn_mysql;
authenticator_provider(#{mechanism := 'password-based', server_type := 'pgsql'}) ->
emqx_authn_pgsql;
authenticator_provider(#{mechanism := 'password-based', server_type := 'mongodb'}) ->
emqx_authn_mongodb;
authenticator_provider(#{mechanism := 'password-based', server_type := 'redis'}) ->
emqx_authn_redis;
authenticator_provider(#{mechanism := 'password-based', server_type := 'http-server'}) ->
emqx_authn_http;
authenticator_provider(#{mechanism := jwt}) ->
emqx_authn_jwt;
authenticator_provider(#{mechanism := scram, server_type := 'built-in-database'}) ->
emqx_enhanced_authn_scram_mnesia.
gen_id(AlreadyExist) ->
ID = list_to_binary(emqx_rule_id:gen()),
case AlreadyExist(ID) of
true -> gen_id(AlreadyExist);
false -> ID
end.
switch_version(State = #{version := ?VER_1}) ->
State#{version := ?VER_2};
switch_version(State = #{version := ?VER_2}) ->
State#{version := ?VER_1};
switch_version(State) ->
State#{version => ?VER_1}.
split_by_name(Name, Config) ->
{Part1, Part2, true} = lists:foldl(
fun(#{<<"name">> := N} = C, {P1, P2, F0}) ->
F = case N =:= Name of
true -> true;
false -> F0
end,
case F of
false -> {[C | P1], P2, F};
true -> {P1, [C | P2], F}
end
end, {[], [], false}, Config),
[Found | NPart2] = lists:reverse(Part2),
{ok, Found, lists:reverse(Part1), NPart2}.
do_create_authenticator(ChainID, AuthenticatorID, #{name := Name} = Config) ->
Provider = authenticator_provider(Config),
Unique = <<ChainID/binary, "/", AuthenticatorID/binary, ":", ?VER_1/binary>>,
case Provider:create(Config#{'_unique' => Unique}) of
{ok, State} ->
Authenticator = #authenticator{id = AuthenticatorID,
name = Name,
provider = Provider,
state = switch_version(State)},
{ok, Authenticator};
{error, Reason} ->
{error, Reason}
end.
do_delete_authenticator(#authenticator{provider = Provider, state = State}) ->
_ = Provider:destroy(State),
ok.
update_or_create_authenticator(ChainID, AuthenticatorID, #{name := NewName} = Config, CreateWhenNotFound) ->
UpdateFun =
fun(#chain{authenticators = Authenticators} = Chain) ->
case lists:keytake(AuthenticatorID, 1, Authenticators) of
false ->
case CreateWhenNotFound of
true ->
case lists:keymember(NewName, 2, Authenticators) of
true ->
{error, name_has_be_used};
false ->
case do_create_authenticator(ChainID, AuthenticatorID, Config) of
{ok, Authenticator} ->
NAuthenticators = Authenticators ++ [{AuthenticatorID, NewName, Authenticator}],
true = ets:insert(?CHAIN_TAB, Chain#chain{authenticators = NAuthenticators}),
{ok, serialize_authenticator(Authenticator)};
{error, Reason} ->
{error, Reason}
end
end;
false ->
{error, {not_found, {authenticator, AuthenticatorID}}}
end;
{value,
{_, _, #authenticator{provider = Provider,
state = #{version := Version} = State} = Authenticator},
Others} ->
case lists:keymember(NewName, 2, Others) of
true ->
{error, name_has_be_used};
false ->
case (NewProvider = authenticator_provider(Config)) =:= Provider of
true ->
Unique = <<ChainID/binary, "/", AuthenticatorID/binary, ":", Version/binary>>,
case Provider:update(Config#{'_unique' => Unique}, State) of
{ok, NewState} ->
NewAuthenticator = Authenticator#authenticator{name = NewName,
state = switch_version(NewState)},
NewAuthenticators = replace_authenticator(AuthenticatorID, NewAuthenticator, Authenticators),
true = ets:insert(?CHAIN_TAB, Chain#chain{authenticators = NewAuthenticators}),
{ok, serialize_authenticator(NewAuthenticator)};
{error, Reason} ->
{error, Reason}
end;
false ->
Unique = <<ChainID/binary, "/", AuthenticatorID/binary, ":", Version/binary>>,
case NewProvider:create(Config#{'_unique' => Unique}) of
{ok, NewState} ->
NewAuthenticator = Authenticator#authenticator{name = NewName,
provider = NewProvider,
state = switch_version(NewState)},
NewAuthenticators = replace_authenticator(AuthenticatorID, NewAuthenticator, Authenticators),
true = ets:insert(?CHAIN_TAB, Chain#chain{authenticators = NewAuthenticators}),
_ = Provider:destroy(State),
{ok, serialize_authenticator(NewAuthenticator)};
{error, Reason} ->
{error, Reason}
end
end
end
end
end,
update_chain(ChainID, UpdateFun).
replace_authenticator(ID, #authenticator{name = Name} = Authenticator, Authenticators) ->
lists:keyreplace(ID, 1, Authenticators, {ID, Name, Authenticator}).
do_move_authenticator(AuthenticatorID, Authenticators, Position) when is_binary(AuthenticatorID) ->
case lists:keytake(AuthenticatorID, 1, Authenticators) of
false ->
{error, {not_found, {authenticator, AuthenticatorID}}};
{value, Authenticator, NAuthenticators} ->
do_move_authenticator(Authenticator, NAuthenticators, Position)
end;
do_move_authenticator(Authenticator, Authenticators, top) ->
{ok, [Authenticator | Authenticators]};
do_move_authenticator(Authenticator, Authenticators, bottom) ->
{ok, Authenticators ++ [Authenticator]};
do_move_authenticator(Authenticator, Authenticators, {before, ID}) ->
insert(Authenticator, Authenticators, ID, []).
insert(_, [], ID, _) ->
{error, {not_found, {authenticator, ID}}};
insert(Authenticator, [{ID, _, _} | _] = Authenticators, ID, Acc) ->
{ok, lists:reverse(Acc) ++ [Authenticator | Authenticators]};
insert(Authenticator, [{_, _, _} = Authenticator0 | More], ID, Acc) ->
insert(Authenticator, More, ID, [Authenticator0 | Acc]).
update_chain(ChainID, UpdateFun) ->
case ets:lookup(?CHAIN_TAB, ChainID) of
[] ->
{error, {not_found, {chain, ChainID}}};
[Chain] ->
UpdateFun(Chain)
end.
call_authenticator(ChainID, AuthenticatorID, Func, Args) ->
UpdateFun =
fun(#chain{authenticators = Authenticators}) ->
case lists:keyfind(AuthenticatorID, 1, Authenticators) of
false ->
{error, {not_found, {authenticator, AuthenticatorID}}};
{_, _, #authenticator{provider = Provider, state = State}} ->
case erlang:function_exported(Provider, Func, length(Args) + 1) of
true ->
erlang:apply(Provider, Func, Args ++ [State]);
false ->
{error, unsupported_feature}
end
end
end,
update_chain(ChainID, UpdateFun).
serialize_chain(#chain{id = ID,
authenticators = Authenticators,
created_at = CreatedAt}) ->
#{id => ID,
authenticators => serialize_authenticators(Authenticators),
created_at => CreatedAt}.
serialize_authenticators(Authenticators) ->
[serialize_authenticator(Authenticator) || {_, _, Authenticator} <- Authenticators].
serialize_authenticator(#authenticator{id = ID,
name = Name,
provider = Provider,
state = State}) ->
#{id => ID, name => Name, provider => Provider, state => State}.

File diff suppressed because it is too large Load Diff

View File

@ -17,7 +17,6 @@
-module(emqx_authn_app).
-include("emqx_authn.hrl").
-include_lib("emqx/include/logger.hrl").
-behaviour(application).
@ -26,33 +25,45 @@
, stop/1
]).
%%------------------------------------------------------------------------------
%% APIs
%%------------------------------------------------------------------------------
start(_StartType, _StartArgs) ->
ok = ekka_rlog:wait_for_shards([?AUTH_SHARD], infinity),
{ok, Sup} = emqx_authn_sup:start_link(),
emqx_config_handler:add_handler([authentication, authenticators], emqx_authn),
initialize(),
ok = add_providers(),
ok = initialize(),
{ok, Sup}.
stop(_State) ->
ok = remove_providers(),
ok.
%%------------------------------------------------------------------------------
%% Internal functions
%%------------------------------------------------------------------------------
add_providers() ->
_ = [?AUTHN:add_provider(AuthNType, Provider) || {AuthNType, Provider} <- providers()], ok.
remove_providers() ->
_ = [?AUTHN:remove_provider(AuthNType) || {AuthNType, _} <- providers()], ok.
initialize() ->
AuthNConfig = emqx:get_config([authentication], #{enable => false,
authenticators => []}),
initialize(AuthNConfig).
initialize(#{enable := Enable, authenticators := AuthenticatorsConfig}) ->
{ok, _} = emqx_authn:create_chain(#{id => ?CHAIN}),
initialize_authenticators(AuthenticatorsConfig),
Enable =:= true andalso emqx_authn:enable(),
?AUTHN:initialize_authentication(?GLOBAL, emqx:get_raw_config([authentication], [])),
lists:foreach(fun({ListenerID, ListenerConfig}) ->
?AUTHN:initialize_authentication(ListenerID, maps:get(authentication, ListenerConfig, []))
end, emqx_listeners:list()),
ok.
initialize_authenticators([]) ->
ok;
initialize_authenticators([#{name := Name} = AuthenticatorConfig | More]) ->
case emqx_authn:create_authenticator(?CHAIN, AuthenticatorConfig) of
{ok, _} ->
initialize_authenticators(More);
{error, Reason} ->
?LOG(error, "Failed to create authenticator '~s': ~p", [Name, Reason])
end.
providers() ->
[ {{'password-based', 'built-in-database'}, emqx_authn_mnesia}
, {{'password-based', mysql}, emqx_authn_mysql}
, {{'password-based', posgresql}, emqx_authn_pgsql}
, {{'password-based', mongodb}, emqx_authn_mongodb}
, {{'password-based', redis}, emqx_authn_redis}
, {{'password-based', 'http-server'}, emqx_authn_http}
, {jwt, emqx_authn_jwt}
, {{scram, 'built-in-database'}, emqx_enhanced_authn_scram_mnesia}
].

View File

@ -16,53 +16,15 @@
-module(emqx_authn_schema).
-include("emqx_authn.hrl").
-include_lib("typerefl/include/types.hrl").
-behaviour(hocon_schema).
-export([ roots/0
, fields/1
-export([ common_fields/0
]).
-export([ authenticator_name/1
]).
%% Export it for emqx_gateway_schema module
-export([ authenticators/1
]).
roots() -> [ "authentication" ].
fields("authentication") ->
common_fields() ->
[ {enable, fun enable/1}
, {authenticators, fun authenticators/1}
].
authenticator_name(type) -> binary();
authenticator_name(nullable) -> false;
authenticator_name(_) -> undefined.
enable(type) -> boolean();
enable(default) -> false;
enable(default) -> true;
enable(_) -> undefined.
authenticators(type) ->
hoconsc:array({union, [ hoconsc:ref(emqx_authn_mnesia, config)
, hoconsc:ref(emqx_authn_mysql, config)
, hoconsc:ref(emqx_authn_pgsql, config)
, hoconsc:ref(emqx_authn_mongodb, standalone)
, hoconsc:ref(emqx_authn_mongodb, 'replica-set')
, hoconsc:ref(emqx_authn_mongodb, 'sharded-cluster')
, hoconsc:ref(emqx_authn_redis, standalone)
, hoconsc:ref(emqx_authn_redis, cluster)
, hoconsc:ref(emqx_authn_redis, sentinel)
, hoconsc:ref(emqx_authn_http, get)
, hoconsc:ref(emqx_authn_http, post)
, hoconsc:ref(emqx_authn_jwt, 'hmac-based')
, hoconsc:ref(emqx_authn_jwt, 'public-key')
, hoconsc:ref(emqx_authn_jwt, 'jwks')
, hoconsc:ref(emqx_enhanced_authn_scram_mnesia, config)
]});
authenticators(default) -> [];
authenticators(_) -> undefined.

View File

@ -26,11 +26,5 @@ start_link() ->
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
init([]) ->
ChildSpecs = [
#{id => emqx_authn,
start => {emqx_authn, start_link, []},
restart => permanent,
type => worker,
modules => [emqx_authn]}
],
ChildSpecs = [],
{ok, {{one_for_one, 10, 10}, ChildSpecs}}.

View File

@ -20,12 +20,15 @@
-include_lib("typerefl/include/types.hrl").
-behaviour(hocon_schema).
-behaviour(emqx_authentication).
-export([ roots/0
-export([ namespace/0
, roots/0
, fields/1
]).
-export([ create/1
-export([ refs/0
, create/1
, update/2
, authenticate/2
, destroy/1
@ -50,7 +53,7 @@
, stored_key
, server_key
, salt
, superuser
, is_superuser
}).
%%------------------------------------------------------------------------------
@ -74,19 +77,16 @@ mnesia(copy) ->
%% Hocon Schema
%%------------------------------------------------------------------------------
namespace() -> "authn:scram:builtin-db".
roots() -> [config].
fields(config) ->
[ {name, fun emqx_authn_schema:authenticator_name/1}
, {mechanism, {enum, [scram]}}
, {server_type, fun server_type/1}
[ {mechanism, {enum, [scram]}}
, {backend, {enum, ['built-in-database']}}
, {algorithm, fun algorithm/1}
, {iteration_count, fun iteration_count/1}
].
server_type(type) -> hoconsc:enum(['built-in-database']);
server_type(default) -> 'built-in-database';
server_type(_) -> undefined.
] ++ emqx_authn_schema:common_fields().
algorithm(type) -> hoconsc:enum([sha256, sha512]);
algorithm(default) -> sha256;
@ -100,6 +100,9 @@ iteration_count(_) -> undefined.
%% APIs
%%------------------------------------------------------------------------------
refs() ->
[hoconsc:ref(?MODULE, config)].
create(#{ algorithm := Algorithm
, iteration_count := IterationCount
, '_unique' := Unique
@ -144,9 +147,9 @@ add_user(#{user_id := UserID,
fun() ->
case mnesia:read(?TAB, {UserGroup, UserID}, write) of
[] ->
Superuser = maps:get(superuser, UserInfo, false),
add_user(UserID, Password, Superuser, State),
{ok, #{user_id => UserID, superuser => Superuser}};
IsSuperuser = maps:get(is_superuser, UserInfo, false),
add_user(UserID, Password, IsSuperuser, State),
{ok, #{user_id => UserID, is_superuser => IsSuperuser}};
[_] ->
{error, already_exist}
end
@ -170,8 +173,8 @@ update_user(UserID, User,
case mnesia:read(?TAB, {UserGroup, UserID}, write) of
[] ->
{error, not_found};
[#user_info{superuser = Superuser} = UserInfo] ->
UserInfo1 = UserInfo#user_info{superuser = maps:get(superuser, User, Superuser)},
[#user_info{is_superuser = IsSuperuser} = UserInfo] ->
UserInfo1 = UserInfo#user_info{is_superuser = maps:get(is_superuser, User, IsSuperuser)},
UserInfo2 = case maps:get(password, User, undefined) of
undefined ->
UserInfo1;
@ -226,24 +229,24 @@ check_client_first_message(Bin, _Cache, #{iteration_count := IterationCount} = S
{error, not_authorized}
end.
check_client_final_message(Bin, #{superuser := Superuser} = Cache, #{algorithm := Alg}) ->
check_client_final_message(Bin, #{is_superuser := IsSuperuser} = Cache, #{algorithm := Alg}) ->
case esasl_scram:check_client_final_message(
Bin,
Cache#{algorithm => Alg}
) of
{ok, ServerFinalMessage} ->
{ok, #{superuser => Superuser}, ServerFinalMessage};
{ok, #{is_superuser => IsSuperuser}, ServerFinalMessage};
{error, _Reason} ->
{error, not_authorized}
end.
add_user(UserID, Password, Superuser, State) ->
add_user(UserID, Password, IsSuperuser, State) ->
{StoredKey, ServerKey, Salt} = esasl_scram:generate_authentication_info(Password, State),
UserInfo = #user_info{user_id = UserID,
stored_key = StoredKey,
server_key = ServerKey,
salt = Salt,
superuser = Superuser},
is_superuser = IsSuperuser},
mnesia:write(?TAB, UserInfo, write).
retrieve(UserID, #{user_group := UserGroup}) ->
@ -251,11 +254,11 @@ retrieve(UserID, #{user_group := UserGroup}) ->
[#user_info{stored_key = StoredKey,
server_key = ServerKey,
salt = Salt,
superuser = Superuser}] ->
is_superuser = IsSuperuser}] ->
{ok, #{stored_key => StoredKey,
server_key => ServerKey,
salt => Salt,
superuser => Superuser}};
is_superuser => IsSuperuser}};
[] ->
{error, not_found}
end.
@ -270,5 +273,5 @@ trans(Fun, Args) ->
{aborted, Reason} -> {error, Reason}
end.
serialize_user_info(#user_info{user_id = {_, UserID}, superuser = Superuser}) ->
#{user_id => UserID, superuser => Superuser}.
serialize_user_info(#user_info{user_id = {_, UserID}, is_superuser = IsSuperuser}) ->
#{user_id => UserID, is_superuser => IsSuperuser}.

View File

@ -21,13 +21,16 @@
-include_lib("typerefl/include/types.hrl").
-behaviour(hocon_schema).
-behaviour(emqx_authentication).
-export([ roots/0
-export([ namespace/0
, roots/0
, fields/1
, validations/0
]).
-export([ create/1
-export([ refs/0
, create/1
, update/2
, authenticate/2
, destroy/1
@ -37,9 +40,11 @@
%% Hocon Schema
%%------------------------------------------------------------------------------
namespace() -> "authn:password-based:http-server".
roots() ->
[ {config, {union, [ hoconsc:t(get)
, hoconsc:t(post)
[ {config, {union, [ hoconsc:ref(?MODULE, get)
, hoconsc:ref(?MODULE, post)
]}}
].
@ -56,13 +61,13 @@ fields(post) ->
] ++ common_fields().
common_fields() ->
[ {name, fun emqx_authn_schema:authenticator_name/1}
, {mechanism, {enum, ['password-based']}}
, {server_type, {enum, ['http-server']}}
[ {mechanism, {enum, ['password-based']}}
, {backend, {enum, ['http-server']}}
, {url, fun url/1}
, {form_data, fun form_data/1}
, {body, fun body/1}
, {request_timeout, fun request_timeout/1}
] ++ maps:to_list(maps:without([ base_url
] ++ emqx_authn_schema:common_fields()
++ maps:to_list(maps:without([ base_url
, pool_type],
maps:from_list(emqx_connector_http:fields(config)))).
@ -92,11 +97,10 @@ headers_no_content_type(converter) ->
headers_no_content_type(default) -> default_headers_no_content_type();
headers_no_content_type(_) -> undefined.
%% TODO: Using map()
form_data(type) -> map();
form_data(nullable) -> false;
form_data(validate) -> [fun check_form_data/1];
form_data(_) -> undefined.
body(type) -> map();
body(nullable) -> false;
body(validate) -> [fun check_body/1];
body(_) -> undefined.
request_timeout(type) -> non_neg_integer();
request_timeout(default) -> 5000;
@ -106,10 +110,15 @@ request_timeout(_) -> undefined.
%% APIs
%%------------------------------------------------------------------------------
refs() ->
[ hoconsc:ref(?MODULE, get)
, hoconsc:ref(?MODULE, post)
].
create(#{ method := Method
, url := URL
, headers := Headers
, form_data := FormData
, body := Body
, request_timeout := RequestTimeout
, '_unique' := Unique
} = Config) ->
@ -118,8 +127,8 @@ create(#{ method := Method
State = #{ method => Method
, path => Path
, base_query => cow_qs:parse_qs(list_to_binary(Query))
, headers => normalize_headers(Headers)
, form_data => maps:to_list(FormData)
, headers => maps:to_list(Headers)
, body => maps:to_list(Body)
, request_timeout => RequestTimeout
, '_unique' => Unique
},
@ -152,16 +161,16 @@ authenticate(Credential, #{'_unique' := Unique,
try
Request = generate_request(Credential, State),
case emqx_resource:query(Unique, {Method, Request, RequestTimeout}) of
{ok, 204, _Headers} -> {ok, #{superuser => false}};
{ok, 204, _Headers} -> {ok, #{is_superuser => false}};
{ok, 200, Headers, Body} ->
ContentType = proplists:get_value(<<"content-type">>, Headers, <<"application/json">>),
case safely_parse_body(ContentType, Body) of
{ok, NBody} ->
%% TODO: Return by user property
{ok, #{superuser => maps:get(<<"superuser">>, NBody, false),
{ok, #{is_superuser => maps:get(<<"is_superuser">>, NBody, false),
user_property => NBody}};
{error, _Reason} ->
{ok, #{superuser => false}}
{ok, #{is_superuser => false}}
end;
{error, _Reason} ->
ignore
@ -186,10 +195,10 @@ check_url(URL) ->
{error, _} -> false
end.
check_form_data(FormData) ->
check_body(Body) ->
lists:any(fun({_, V}) ->
not is_binary(V)
end, maps:to_list(FormData)).
end, maps:to_list(Body)).
default_headers() ->
maps:put(<<"content-type">>,
@ -229,24 +238,21 @@ parse_url(URL) ->
URIMap
end.
normalize_headers(Headers) ->
[{atom_to_binary(K), V} || {K, V} <- maps:to_list(Headers)].
generate_request(Credential, #{method := Method,
path := Path,
base_query := BaseQuery,
headers := Headers,
form_data := FormData0}) ->
FormData = replace_placeholders(FormData0, Credential),
body := Body0}) ->
Body = replace_placeholders(Body0, Credential),
case Method of
get ->
NPath = append_query(Path, BaseQuery ++ FormData),
NPath = append_query(Path, BaseQuery ++ Body),
{NPath, Headers};
post ->
NPath = append_query(Path, BaseQuery),
ContentType = proplists:get_value(<<"content-type">>, Headers),
Body = serialize_body(ContentType, FormData),
{NPath, Headers, Body}
NBody = serialize_body(ContentType, Body),
{NPath, Headers, NBody}
end.
replace_placeholders(KVs, Credential) ->
@ -276,10 +282,10 @@ qs([], Acc) ->
qs([{K, V} | More], Acc) ->
qs(More, [["&", emqx_http_lib:uri_encode(K), "=", emqx_http_lib:uri_encode(V)] | Acc]).
serialize_body(<<"application/json">>, FormData) ->
emqx_json:encode(FormData);
serialize_body(<<"application/x-www-form-urlencoded">>, FormData) ->
qs(FormData).
serialize_body(<<"application/json">>, Body) ->
emqx_json:encode(Body);
serialize_body(<<"application/x-www-form-urlencoded">>, Body) ->
qs(Body).
safely_parse_body(ContentType, Body) ->
try parse_body(ContentType, Body) of

View File

@ -19,12 +19,15 @@
-include_lib("typerefl/include/types.hrl").
-behaviour(hocon_schema).
-behaviour(emqx_authentication).
-export([ roots/0
-export([ namespace/0
, roots/0
, fields/1
]).
-export([ create/1
-export([ refs/0
, create/1
, update/2
, authenticate/2
, destroy/1
@ -34,10 +37,12 @@
%% Hocon Schema
%%------------------------------------------------------------------------------
namespace() -> "authn:jwt".
roots() ->
[ {config, {union, [ hoconsc:t('hmac-based')
, hoconsc:t('public-key')
, hoconsc:t('jwks')
[ {config, {union, [ hoconsc:mk('hmac-based')
, hoconsc:mk('public-key')
, hoconsc:mk('jwks')
]}}
].
@ -78,12 +83,11 @@ fields(ssl_disable) ->
[ {enable, #{type => false}} ].
common_fields() ->
[ {name, fun emqx_authn_schema:authenticator_name/1}
, {mechanism, {enum, [jwt]}}
[ {mechanism, {enum, [jwt]}}
, {verify_claims, fun verify_claims/1}
].
] ++ emqx_authn_schema:common_fields().
secret(type) -> string();
secret(type) -> binary();
secret(_) -> undefined.
secret_base64_encoded(type) -> boolean();
@ -130,6 +134,12 @@ verify_claims(_) -> undefined.
%% APIs
%%------------------------------------------------------------------------------
refs() ->
[ hoconsc:ref(?MODULE, 'hmac-based')
, hoconsc:ref(?MODULE, 'public-key')
, hoconsc:ref(?MODULE, 'jwks')
].
create(#{verify_claims := VerifyClaims} = Config) ->
create2(Config#{verify_claims => handle_verify_claims(VerifyClaims)}).
@ -239,7 +249,7 @@ verify(JWS, [JWK | More], VerifyClaims) ->
Claims = emqx_json:decode(Payload, [return_maps]),
case verify_claims(Claims, VerifyClaims) of
ok ->
{ok, #{superuser => maps:get(<<"superuser">>, Claims, false)}};
{ok, #{is_superuser => maps:get(<<"is_superuser">>, Claims, false)}};
{error, Reason} ->
{error, Reason}
end;

View File

@ -20,10 +20,15 @@
-include_lib("typerefl/include/types.hrl").
-behaviour(hocon_schema).
-behaviour(emqx_authentication).
-export([ roots/0, fields/1 ]).
-export([ namespace/0
, roots/0
, fields/1
]).
-export([ create/1
-export([ refs/0
, create/1
, update/2
, authenticate/2
, destroy/1
@ -46,7 +51,7 @@
{ user_id :: {user_group(), user_id()}
, password_hash :: binary()
, salt :: binary()
, superuser :: boolean()
, is_superuser :: boolean()
}).
-reflect_type([ user_id_type/0 ]).
@ -79,15 +84,16 @@ mnesia(copy) ->
%% Hocon Schema
%%------------------------------------------------------------------------------
namespace() -> "authn:password-based:builtin-db".
roots() -> [config].
fields(config) ->
[ {name, fun emqx_authn_schema:authenticator_name/1}
, {mechanism, {enum, ['password-based']}}
, {server_type, {enum, ['built-in-database']}}
[ {mechanism, {enum, ['password-based']}}
, {backend, {enum, ['built-in-database']}}
, {user_id_type, fun user_id_type/1}
, {password_hash_algorithm, fun password_hash_algorithm/1}
];
] ++ emqx_authn_schema:common_fields();
fields(bcrypt) ->
[ {name, {enum, [bcrypt]}}
@ -102,7 +108,8 @@ user_id_type(type) -> user_id_type();
user_id_type(default) -> username;
user_id_type(_) -> undefined.
password_hash_algorithm(type) -> {union, [hoconsc:ref(bcrypt), hoconsc:ref(other_algorithms)]};
password_hash_algorithm(type) -> hoconsc:union([hoconsc:ref(?MODULE, bcrypt),
hoconsc:ref(?MODULE, other_algorithms)]);
password_hash_algorithm(default) -> #{<<"name">> => sha256};
password_hash_algorithm(_) -> undefined.
@ -114,6 +121,9 @@ salt_rounds(_) -> undefined.
%% APIs
%%------------------------------------------------------------------------------
refs() ->
[hoconsc:ref(?MODULE, config)].
create(#{ user_id_type := Type
, password_hash_algorithm := #{name := bcrypt,
salt_rounds := SaltRounds}
@ -148,13 +158,13 @@ authenticate(#{password := Password} = Credential,
case mnesia:dirty_read(?TAB, {UserGroup, UserID}) of
[] ->
ignore;
[#user_info{password_hash = PasswordHash, salt = Salt0, superuser = Superuser}] ->
[#user_info{password_hash = PasswordHash, salt = Salt0, is_superuser = IsSuperuser}] ->
Salt = case Algorithm of
bcrypt -> PasswordHash;
_ -> Salt0
end,
case PasswordHash =:= hash(Algorithm, Password, Salt) of
true -> {ok, #{superuser => Superuser}};
true -> {ok, #{is_superuser => IsSuperuser}};
false -> {error, bad_username_or_password}
end
end.
@ -187,9 +197,9 @@ add_user(#{user_id := UserID,
case mnesia:read(?TAB, {UserGroup, UserID}, write) of
[] ->
{PasswordHash, Salt} = hash(Password, State),
Superuser = maps:get(superuser, UserInfo, false),
insert_user(UserGroup, UserID, PasswordHash, Salt, Superuser),
{ok, #{user_id => UserID, superuser => Superuser}};
IsSuperuser = maps:get(is_superuser, UserInfo, false),
insert_user(UserGroup, UserID, PasswordHash, Salt, IsSuperuser),
{ok, #{user_id => UserID, is_superuser => IsSuperuser}};
[_] ->
{error, already_exist}
end
@ -215,8 +225,8 @@ update_user(UserID, UserInfo,
{error, not_found};
[#user_info{ password_hash = PasswordHash
, salt = Salt
, superuser = Superuser}] ->
NSuperuser = maps:get(superuser, UserInfo, Superuser),
, is_superuser = IsSuperuser}] ->
NSuperuser = maps:get(is_superuser, UserInfo, IsSuperuser),
{NPasswordHash, NSalt} = case maps:get(password, UserInfo, undefined) of
undefined ->
{PasswordHash, Salt};
@ -224,7 +234,7 @@ update_user(UserID, UserInfo,
hash(Password, State)
end,
insert_user(UserGroup, UserID, NPasswordHash, NSalt, NSuperuser),
{ok, #{user_id => UserID, superuser => NSuperuser}}
{ok, #{user_id => UserID, is_superuser => NSuperuser}}
end
end).
@ -280,8 +290,8 @@ import(UserGroup, [#{<<"user_id">> := UserID,
<<"password_hash">> := PasswordHash} = UserInfo | More])
when is_binary(UserID) andalso is_binary(PasswordHash) ->
Salt = maps:get(<<"salt">>, UserInfo, <<>>),
Superuser = maps:get(<<"superuser">>, UserInfo, false),
insert_user(UserGroup, UserID, PasswordHash, Salt, Superuser),
IsSuperuser = maps:get(<<"is_superuser">>, UserInfo, false),
insert_user(UserGroup, UserID, PasswordHash, Salt, IsSuperuser),
import(UserGroup, More);
import(_UserGroup, [_ | _More]) ->
{error, bad_format}.
@ -295,8 +305,8 @@ import(UserGroup, File, Seq) ->
{ok, #{user_id := UserID,
password_hash := PasswordHash} = UserInfo} ->
Salt = maps:get(salt, UserInfo, <<>>),
Superuser = maps:get(superuser, UserInfo, false),
insert_user(UserGroup, UserID, PasswordHash, Salt, Superuser),
IsSuperuser = maps:get(is_superuser, UserInfo, false),
insert_user(UserGroup, UserID, PasswordHash, Salt, IsSuperuser),
import(UserGroup, File, Seq);
{error, Reason} ->
{error, Reason}
@ -331,10 +341,10 @@ get_user_info_by_seq([PasswordHash | More1], [<<"password_hash">> | More2], Acc)
get_user_info_by_seq(More1, More2, Acc#{password_hash => PasswordHash});
get_user_info_by_seq([Salt | More1], [<<"salt">> | More2], Acc) ->
get_user_info_by_seq(More1, More2, Acc#{salt => Salt});
get_user_info_by_seq([<<"true">> | More1], [<<"superuser">> | More2], Acc) ->
get_user_info_by_seq(More1, More2, Acc#{superuser => true});
get_user_info_by_seq([<<"false">> | More1], [<<"superuser">> | More2], Acc) ->
get_user_info_by_seq(More1, More2, Acc#{superuser => false});
get_user_info_by_seq([<<"true">> | More1], [<<"is_superuser">> | More2], Acc) ->
get_user_info_by_seq(More1, More2, Acc#{is_superuser => true});
get_user_info_by_seq([<<"false">> | More1], [<<"is_superuser">> | More2], Acc) ->
get_user_info_by_seq(More1, More2, Acc#{is_superuser => false});
get_user_info_by_seq(_, _, _) ->
{error, bad_format}.
@ -358,11 +368,11 @@ hash(Password, #{password_hash_algorithm := Algorithm} = State) ->
PasswordHash = hash(Algorithm, Password, Salt),
{PasswordHash, Salt}.
insert_user(UserGroup, UserID, PasswordHash, Salt, Superuser) ->
insert_user(UserGroup, UserID, PasswordHash, Salt, IsSuperuser) ->
UserInfo = #user_info{user_id = {UserGroup, UserID},
password_hash = PasswordHash,
salt = Salt,
superuser = Superuser},
is_superuser = IsSuperuser},
mnesia:write(?TAB, UserInfo, write).
delete_user2(UserInfo) ->
@ -390,5 +400,5 @@ to_binary(B) when is_binary(B) ->
to_binary(L) when is_list(L) ->
iolist_to_binary(L).
serialize_user_info(#user_info{user_id = {_, UserID}, superuser = Superuser}) ->
#{user_id => UserID, superuser => Superuser}.
serialize_user_info(#user_info{user_id = {_, UserID}, is_superuser = IsSuperuser}) ->
#{user_id => UserID, is_superuser => IsSuperuser}.

View File

@ -21,12 +21,15 @@
-include_lib("typerefl/include/types.hrl").
-behaviour(hocon_schema).
-behaviour(emqx_authentication).
-export([ roots/0
-export([ namespace/0
, roots/0
, fields/1
]).
-export([ create/1
-export([ refs/0
, create/1
, update/2
, authenticate/2
, destroy/1
@ -36,10 +39,12 @@
%% Hocon Schema
%%------------------------------------------------------------------------------
namespace() -> "authn:password-based:mongodb".
roots() ->
[ {config, {union, [ hoconsc:t(standalone)
, hoconsc:t('replica-set')
, hoconsc:t('sharded-cluster')
[ {config, {union, [ hoconsc:mk(standalone)
, hoconsc:mk('replica-set')
, hoconsc:mk('sharded-cluster')
]}}
].
@ -53,16 +58,16 @@ fields('sharded-cluster') ->
common_fields() ++ emqx_connector_mongo:fields(sharded).
common_fields() ->
[ {name, fun emqx_authn_schema:authenticator_name/1}
, {mechanism, {enum, ['password-based']}}
, {server_type, {enum, [mongodb]}}
[ {mechanism, {enum, ['password-based']}}
, {backend, {enum, [mongodb]}}
, {collection, fun collection/1}
, {selector, fun selector/1}
, {password_hash_field, fun password_hash_field/1}
, {salt_field, fun salt_field/1}
, {is_superuser_field, fun is_superuser_field/1}
, {password_hash_algorithm, fun password_hash_algorithm/1}
, {salt_position, fun salt_position/1}
].
] ++ emqx_authn_schema:common_fields().
collection(type) -> binary();
collection(nullable) -> false;
@ -80,6 +85,10 @@ salt_field(type) -> binary();
salt_field(nullable) -> true;
salt_field(_) -> undefined.
is_superuser_field(type) -> binary();
is_superuser_field(nullable) -> true;
is_superuser_field(_) -> undefined.
password_hash_algorithm(type) -> {enum, [plain, md5, sha, sha256, sha512, bcrypt]};
password_hash_algorithm(default) -> sha256;
password_hash_algorithm(_) -> undefined.
@ -92,6 +101,12 @@ salt_position(_) -> undefined.
%% APIs
%%------------------------------------------------------------------------------
refs() ->
[ hoconsc:ref(?MODULE, standalone)
, hoconsc:ref(?MODULE, 'replica-set')
, hoconsc:ref(?MODULE, 'sharded-cluster')
].
create(#{ selector := Selector
, '_unique' := Unique
} = Config) ->
@ -99,6 +114,7 @@ create(#{ selector := Selector
State = maps:with([ collection
, password_hash_field
, salt_field
, is_superuser_field
, password_hash_algorithm
, salt_position
, '_unique'], Config),
@ -139,7 +155,7 @@ authenticate(#{password := Password} = Credential,
Doc ->
case check_password(Password, Doc, State) of
ok ->
{ok, #{superuser => superuser(Doc, State)}};
{ok, #{is_superuser => is_superuser(Doc, State)}};
{error, {cannot_find_password_hash_field, PasswordHashField}} ->
?LOG(error, "['~s'] Can't find password hash field: ~s", [Unique, PasswordHashField]),
{error, bad_username_or_password};
@ -220,9 +236,9 @@ check_password(Password,
end
end.
superuser(Doc, #{superuser_field := SuperuserField}) ->
maps:get(SuperuserField, Doc, false);
superuser(_, _) ->
is_superuser(Doc, #{is_superuser_field := IsSuperuserField}) ->
maps:get(IsSuperuserField, Doc, false);
is_superuser(_, _) ->
false.
hash(Algorithm, Password, Salt, prefix) ->

View File

@ -21,12 +21,15 @@
-include_lib("typerefl/include/types.hrl").
-behaviour(hocon_schema).
-behaviour(emqx_authentication).
-export([ roots/0
-export([ namespace/0
, roots/0
, fields/1
]).
-export([ create/1
-export([ refs/0
, create/1
, update/2
, authenticate/2
, destroy/1
@ -36,17 +39,19 @@
%% Hocon Schema
%%------------------------------------------------------------------------------
namespace() -> "authn:password-based:mysql".
roots() -> [config].
fields(config) ->
[ {name, fun emqx_authn_schema:authenticator_name/1}
, {mechanism, {enum, ['password-based']}}
, {server_type, {enum, [mysql]}}
[ {mechanism, {enum, ['password-based']}}
, {backend, {enum, [mysql]}}
, {password_hash_algorithm, fun password_hash_algorithm/1}
, {salt_position, fun salt_position/1}
, {query, fun query/1}
, {query_timeout, fun query_timeout/1}
] ++ emqx_connector_schema_lib:relational_db_fields()
] ++ emqx_authn_schema:common_fields()
++ emqx_connector_schema_lib:relational_db_fields()
++ emqx_connector_schema_lib:ssl_fields().
password_hash_algorithm(type) -> {enum, [plain, md5, sha, sha256, sha512, bcrypt]};
@ -69,6 +74,9 @@ query_timeout(_) -> undefined.
%% APIs
%%------------------------------------------------------------------------------
refs() ->
[hoconsc:ref(?MODULE, config)].
create(#{ password_hash_algorithm := Algorithm
, salt_position := SaltPosition
, query := Query0
@ -115,7 +123,7 @@ authenticate(#{password := Password} = Credential,
Selected = maps:from_list(lists:zip(Columns, Rows)),
case check_password(Password, Selected, State) of
ok ->
{ok, #{superuser => maps:get(<<"superuser">>, Selected, false)}};
{ok, #{is_superuser => maps:get(<<"is_superuser">>, Selected, false)}};
{error, Reason} ->
{error, Reason}
end;

View File

@ -22,10 +22,15 @@
-include_lib("typerefl/include/types.hrl").
-behaviour(hocon_schema).
-behaviour(emqx_authentication).
-export([ roots/0, fields/1 ]).
-export([ namespace/0
, roots/0
, fields/1
]).
-export([ create/1
-export([ refs/0
, create/1
, update/2
, authenticate/2
, destroy/1
@ -35,16 +40,18 @@
%% Hocon Schema
%%------------------------------------------------------------------------------
namespace() -> "authn:password-based:postgresql".
roots() -> [config].
fields(config) ->
[ {name, fun emqx_authn_schema:authenticator_name/1}
, {mechanism, {enum, ['password-based']}}
, {server_type, {enum, [pgsql]}}
[ {mechanism, {enum, ['password-based']}}
, {backend, {enum, [postgresql]}}
, {password_hash_algorithm, fun password_hash_algorithm/1}
, {salt_position, {enum, [prefix, suffix]}}
, {query, fun query/1}
] ++ emqx_connector_schema_lib:relational_db_fields()
] ++ emqx_authn_schema:common_fields()
++ emqx_connector_schema_lib:relational_db_fields()
++ emqx_connector_schema_lib:ssl_fields().
password_hash_algorithm(type) -> {enum, [plain, md5, sha, sha256, sha512, bcrypt]};
@ -59,6 +66,9 @@ query(_) -> undefined.
%% APIs
%%------------------------------------------------------------------------------
refs() ->
[hoconsc:ref(?MODULE, config)].
create(#{ query := Query0
, password_hash_algorithm := Algorithm
, salt_position := SaltPosition
@ -103,7 +113,7 @@ authenticate(#{password := Password} = Credential,
Selected = maps:from_list(lists:zip(NColumns, Rows)),
case check_password(Password, Selected, State) of
ok ->
{ok, #{superuser => maps:get(<<"superuser">>, Selected, false)}};
{ok, #{is_superuser => maps:get(<<"is_superuser">>, Selected, false)}};
{error, Reason} ->
{error, Reason}
end;

View File

@ -21,12 +21,15 @@
-include_lib("typerefl/include/types.hrl").
-behaviour(hocon_schema).
-behaviour(emqx_authentication).
-export([ roots/0
-export([ namespace/0
, roots/0
, fields/1
]).
-export([ create/1
-export([ refs/0
, create/1
, update/2
, authenticate/2
, destroy/1
@ -36,10 +39,12 @@
%% Hocon Schema
%%------------------------------------------------------------------------------
namespace() -> "authn:password-based:redis".
roots() ->
[ {config, {union, [ hoconsc:t(standalone)
, hoconsc:t(cluster)
, hoconsc:t(sentinel)
[ {config, {union, [ hoconsc:mk(standalone)
, hoconsc:mk(cluster)
, hoconsc:mk(sentinel)
]}}
].
@ -53,13 +58,12 @@ fields(sentinel) ->
common_fields() ++ emqx_connector_redis:fields(sentinel).
common_fields() ->
[ {name, fun emqx_authn_schema:authenticator_name/1}
, {mechanism, {enum, ['password-based']}}
, {server_type, {enum, [redis]}}
[ {mechanism, {enum, ['password-based']}}
, {backend, {enum, [redis]}}
, {query, fun query/1}
, {password_hash_algorithm, fun password_hash_algorithm/1}
, {salt_position, fun salt_position/1}
].
] ++ emqx_authn_schema:common_fields().
query(type) -> string();
query(nullable) -> false;
@ -77,6 +81,12 @@ salt_position(_) -> undefined.
%% APIs
%%------------------------------------------------------------------------------
refs() ->
[ hoconsc:ref(?MODULE, standalone)
, hoconsc:ref(?MODULE, cluster)
, hoconsc:ref(?MODULE, sentinel)
].
create(#{ query := Query
, '_unique' := Unique
} = Config) ->
@ -125,7 +135,7 @@ authenticate(#{password := Password} = Credential,
Selected = merge(Fields, Values),
case check_password(Password, Selected, State) of
ok ->
{ok, #{superuser => maps:get("superuser", Selected, false)}};
{ok, #{is_superuser => maps:get("is_superuser", Selected, false)}};
{error, Reason} ->
{error, Reason}
end;
@ -170,7 +180,7 @@ check_fields(["password_hash" | More], false) ->
check_fields(More, true);
check_fields(["salt" | More], HasPassHash) ->
check_fields(More, HasPassHash);
check_fields(["superuser" | More], HasPassHash) ->
check_fields(["is_superuser" | More], HasPassHash) ->
check_fields(More, HasPassHash);
check_fields([Field | _], _) ->
error({unsupported_field, Field}).

View File

@ -1,3 +1,3 @@
user_id,password_hash,salt,superuser
user_id,password_hash,salt,is_superuser
myuser3,b6c743545a7817ae8c8f624371d5f5f0373234bb0ff36b8ffbf19bce0e06ab75,de1024f462fb83910fd13151bd4bd235,true
myuser4,ee68c985a69208b6eda8c6c9b4c7c2d2b15ee2352cdd64a903171710a99182e8,ad773b5be9dd0613fe6c2f4d8c403139,false

1 user_id password_hash salt superuser is_superuser
2 myuser3 b6c743545a7817ae8c8f624371d5f5f0373234bb0ff36b8ffbf19bce0e06ab75 de1024f462fb83910fd13151bd4bd235 true true
3 myuser4 ee68c985a69208b6eda8c6c9b4c7c2d2b15ee2352cdd64a903171710a99182e8 ad773b5be9dd0613fe6c2f4d8c403139 false false

View File

@ -3,12 +3,12 @@
"user_id":"myuser1",
"password_hash":"c5e46903df45e5dc096dc74657610dbee8deaacae656df88a1788f1847390242",
"salt": "e378187547bf2d6f0545a3f441aa4d8a",
"superuser": true
"is_superuser": true
},
{
"user_id":"myuser2",
"password_hash":"f4d17f300b11e522fd33f497c11b126ef1ea5149c74d2220f9a16dc876d4567b",
"salt": "6d3f9bd5b54d94b98adbcfe10b6d181f",
"superuser": false
"is_superuser": false
}
]

View File

@ -19,97 +19,4 @@
-compile(export_all).
-compile(nowarn_export_all).
-include_lib("common_test/include/ct.hrl").
-include_lib("eunit/include/eunit.hrl").
-include("emqx_authn.hrl").
-define(AUTH, emqx_authn).
all() ->
emqx_ct:all(?MODULE).
init_per_suite(Config) ->
application:set_env(ekka, strict_mode, true),
emqx_ct_helpers:start_apps([emqx_authn]),
Config.
end_per_suite(_) ->
emqx_ct_helpers:stop_apps([emqx_authn]),
ok.
t_chain(_) ->
?assertMatch({ok, #{id := ?CHAIN, authenticators := []}}, ?AUTH:lookup_chain(?CHAIN)),
ChainID = <<"mychain">>,
Chain = #{id => ChainID},
?assertMatch({ok, #{id := ChainID, authenticators := []}}, ?AUTH:create_chain(Chain)),
?assertEqual({error, {already_exists, {chain, ChainID}}}, ?AUTH:create_chain(Chain)),
?assertMatch({ok, #{id := ChainID, authenticators := []}}, ?AUTH:lookup_chain(ChainID)),
?assertEqual(ok, ?AUTH:delete_chain(ChainID)),
?assertMatch({error, {not_found, {chain, ChainID}}}, ?AUTH:lookup_chain(ChainID)),
ok.
t_authenticator(_) ->
AuthenticatorName1 = <<"myauthenticator1">>,
AuthenticatorConfig1 = #{name => AuthenticatorName1,
mechanism => 'password-based',
server_type => 'built-in-database',
user_id_type => username,
password_hash_algorithm => #{
name => sha256
}},
{ok, #{name := AuthenticatorName1, id := ID1}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig1),
?assertMatch({ok, #{name := AuthenticatorName1}}, ?AUTH:lookup_authenticator(?CHAIN, ID1)),
?assertMatch({ok, [#{name := AuthenticatorName1}]}, ?AUTH:list_authenticators(?CHAIN)),
?assertEqual({error, name_has_be_used}, ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig1)),
AuthenticatorConfig2 = #{name => AuthenticatorName1,
mechanism => jwt,
use_jwks => false,
algorithm => 'hmac-based',
secret => <<"abcdef">>,
secret_base64_encoded => false,
verify_claims => []},
{ok, #{name := AuthenticatorName1, id := ID1}} = ?AUTH:update_authenticator(?CHAIN, ID1, AuthenticatorConfig2),
ID2 = <<"random">>,
?assertEqual({error, {not_found, {authenticator, ID2}}}, ?AUTH:update_authenticator(?CHAIN, ID2, AuthenticatorConfig2)),
?assertEqual({error, name_has_be_used}, ?AUTH:update_or_create_authenticator(?CHAIN, ID2, AuthenticatorConfig2)),
AuthenticatorName2 = <<"myauthenticator2">>,
AuthenticatorConfig3 = AuthenticatorConfig2#{name => AuthenticatorName2},
{ok, #{name := AuthenticatorName2, id := ID2}} = ?AUTH:update_or_create_authenticator(?CHAIN, ID2, AuthenticatorConfig3),
?assertMatch({ok, #{name := AuthenticatorName2}}, ?AUTH:lookup_authenticator(?CHAIN, ID2)),
{ok, #{name := AuthenticatorName2, id := ID2}} = ?AUTH:update_or_create_authenticator(?CHAIN, ID2, AuthenticatorConfig3#{secret := <<"fedcba">>}),
?assertMatch({ok, #{id := ?CHAIN, authenticators := [#{name := AuthenticatorName1}, #{name := AuthenticatorName2}]}}, ?AUTH:lookup_chain(?CHAIN)),
?assertMatch({ok, [#{name := AuthenticatorName1}, #{name := AuthenticatorName2}]}, ?AUTH:list_authenticators(?CHAIN)),
?assertEqual(ok, ?AUTH:move_authenticator(?CHAIN, ID2, top)),
?assertMatch({ok, [#{name := AuthenticatorName2}, #{name := AuthenticatorName1}]}, ?AUTH:list_authenticators(?CHAIN)),
?assertEqual(ok, ?AUTH:move_authenticator(?CHAIN, ID2, bottom)),
?assertMatch({ok, [#{name := AuthenticatorName1}, #{name := AuthenticatorName2}]}, ?AUTH:list_authenticators(?CHAIN)),
?assertEqual(ok, ?AUTH:move_authenticator(?CHAIN, ID2, {before, ID1})),
?assertMatch({ok, [#{name := AuthenticatorName2}, #{name := AuthenticatorName1}]}, ?AUTH:list_authenticators(?CHAIN)),
?assertEqual({error, {not_found, {authenticator, <<"nonexistent">>}}}, ?AUTH:move_authenticator(?CHAIN, ID2, {before, <<"nonexistent">>})),
?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID1)),
?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID2)),
?assertEqual({ok, []}, ?AUTH:list_authenticators(?CHAIN)),
ok.
t_authenticate(_) ->
ClientInfo = #{zone => default,
listener => {tcp, default},
username => <<"myuser">>,
password => <<"mypass">>},
?assertEqual({ok, #{superuser => false}}, emqx_access_control:authenticate(ClientInfo)),
?assertEqual(false, emqx_authn:is_enabled()),
emqx_authn:enable(),
?assertEqual(true, emqx_authn:is_enabled()),
?assertEqual({error, not_authorized}, emqx_access_control:authenticate(ClientInfo)).
all() -> emqx_ct:all(?MODULE).

View File

@ -19,140 +19,140 @@
-compile(export_all).
-compile(nowarn_export_all).
-include_lib("common_test/include/ct.hrl").
-include_lib("eunit/include/eunit.hrl").
% -include_lib("common_test/include/ct.hrl").
% -include_lib("eunit/include/eunit.hrl").
-include("emqx_authn.hrl").
% -include("emqx_authn.hrl").
-define(AUTH, emqx_authn).
% -define(AUTH, emqx_authn).
all() ->
emqx_ct:all(?MODULE).
init_per_suite(Config) ->
emqx_ct_helpers:start_apps([emqx_authn]),
Config.
% init_per_suite(Config) ->
% emqx_ct_helpers:start_apps([emqx_authn]),
% Config.
end_per_suite(_) ->
emqx_ct_helpers:stop_apps([emqx_authn]),
ok.
% end_per_suite(_) ->
% emqx_ct_helpers:stop_apps([emqx_authn]),
% ok.
t_jwt_authenticator(_) ->
AuthenticatorName = <<"myauthenticator">>,
Config = #{name => AuthenticatorName,
mechanism => jwt,
use_jwks => false,
algorithm => 'hmac-based',
secret => <<"abcdef">>,
secret_base64_encoded => false,
verify_claims => []},
{ok, #{name := AuthenticatorName, id := ID}} = ?AUTH:create_authenticator(?CHAIN, Config),
% t_jwt_authenticator(_) ->
% AuthenticatorName = <<"myauthenticator">>,
% Config = #{name => AuthenticatorName,
% mechanism => jwt,
% use_jwks => false,
% algorithm => 'hmac-based',
% secret => <<"abcdef">>,
% secret_base64_encoded => false,
% verify_claims => []},
% {ok, #{name := AuthenticatorName, id := ID}} = ?AUTH:create_authenticator(?CHAIN, Config),
Payload = #{<<"username">> => <<"myuser">>},
JWS = generate_jws('hmac-based', Payload, <<"abcdef">>),
ClientInfo = #{username => <<"myuser">>,
password => JWS},
?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)),
% Payload = #{<<"username">> => <<"myuser">>},
% JWS = generate_jws('hmac-based', Payload, <<"abcdef">>),
% ClientInfo = #{username => <<"myuser">>,
% password => JWS},
% ?assertEqual({stop, {ok, #{is_superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)),
Payload1 = #{<<"username">> => <<"myuser">>, <<"superuser">> => true},
JWS1 = generate_jws('hmac-based', Payload1, <<"abcdef">>),
ClientInfo1 = #{username => <<"myuser">>,
password => JWS1},
?assertEqual({stop, {ok, #{superuser => true}}}, ?AUTH:authenticate(ClientInfo1, ignored)),
% Payload1 = #{<<"username">> => <<"myuser">>, <<"is_superuser">> => true},
% JWS1 = generate_jws('hmac-based', Payload1, <<"abcdef">>),
% ClientInfo1 = #{username => <<"myuser">>,
% password => JWS1},
% ?assertEqual({stop, {ok, #{is_superuser => true}}}, ?AUTH:authenticate(ClientInfo1, ignored)),
BadJWS = generate_jws('hmac-based', Payload, <<"bad_secret">>),
ClientInfo2 = ClientInfo#{password => BadJWS},
?assertEqual({stop, {error, not_authorized}}, ?AUTH:authenticate(ClientInfo2, ignored)),
% BadJWS = generate_jws('hmac-based', Payload, <<"bad_secret">>),
% ClientInfo2 = ClientInfo#{password => BadJWS},
% ?assertEqual({stop, {error, not_authorized}}, ?AUTH:authenticate(ClientInfo2, ignored)),
%% secret_base64_encoded
Config2 = Config#{secret => base64:encode(<<"abcdef">>),
secret_base64_encoded => true},
?assertMatch({ok, _}, ?AUTH:update_authenticator(?CHAIN, ID, Config2)),
?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)),
% %% secret_base64_encoded
% Config2 = Config#{secret => base64:encode(<<"abcdef">>),
% secret_base64_encoded => true},
% ?assertMatch({ok, _}, ?AUTH:update_authenticator(?CHAIN, ID, Config2)),
% ?assertEqual({stop, {ok, #{is_superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)),
Config3 = Config#{verify_claims => [{<<"username">>, <<"${mqtt-username}">>}]},
?assertMatch({ok, _}, ?AUTH:update_authenticator(?CHAIN, ID, Config3)),
?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)),
?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo#{username => <<"otheruser">>}, ok)),
% Config3 = Config#{verify_claims => [{<<"username">>, <<"${mqtt-username}">>}]},
% ?assertMatch({ok, _}, ?AUTH:update_authenticator(?CHAIN, ID, Config3)),
% ?assertEqual({stop, {ok, #{is_superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)),
% ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo#{username => <<"otheruser">>}, ok)),
%% Expiration
Payload3 = #{ <<"username">> => <<"myuser">>
, <<"exp">> => erlang:system_time(second) - 60},
JWS3 = generate_jws('hmac-based', Payload3, <<"abcdef">>),
ClientInfo3 = ClientInfo#{password => JWS3},
?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo3, ignored)),
% %% Expiration
% Payload3 = #{ <<"username">> => <<"myuser">>
% , <<"exp">> => erlang:system_time(second) - 60},
% JWS3 = generate_jws('hmac-based', Payload3, <<"abcdef">>),
% ClientInfo3 = ClientInfo#{password => JWS3},
% ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo3, ignored)),
Payload4 = #{ <<"username">> => <<"myuser">>
, <<"exp">> => erlang:system_time(second) + 60},
JWS4 = generate_jws('hmac-based', Payload4, <<"abcdef">>),
ClientInfo4 = ClientInfo#{password => JWS4},
?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo4, ignored)),
% Payload4 = #{ <<"username">> => <<"myuser">>
% , <<"exp">> => erlang:system_time(second) + 60},
% JWS4 = generate_jws('hmac-based', Payload4, <<"abcdef">>),
% ClientInfo4 = ClientInfo#{password => JWS4},
% ?assertEqual({stop, {ok, #{is_superuser => false}}}, ?AUTH:authenticate(ClientInfo4, ignored)),
%% Issued At
Payload5 = #{ <<"username">> => <<"myuser">>
, <<"iat">> => erlang:system_time(second) - 60},
JWS5 = generate_jws('hmac-based', Payload5, <<"abcdef">>),
ClientInfo5 = ClientInfo#{password => JWS5},
?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo5, ignored)),
% %% Issued At
% Payload5 = #{ <<"username">> => <<"myuser">>
% , <<"iat">> => erlang:system_time(second) - 60},
% JWS5 = generate_jws('hmac-based', Payload5, <<"abcdef">>),
% ClientInfo5 = ClientInfo#{password => JWS5},
% ?assertEqual({stop, {ok, #{is_superuser => false}}}, ?AUTH:authenticate(ClientInfo5, ignored)),
Payload6 = #{ <<"username">> => <<"myuser">>
, <<"iat">> => erlang:system_time(second) + 60},
JWS6 = generate_jws('hmac-based', Payload6, <<"abcdef">>),
ClientInfo6 = ClientInfo#{password => JWS6},
?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo6, ignored)),
% Payload6 = #{ <<"username">> => <<"myuser">>
% , <<"iat">> => erlang:system_time(second) + 60},
% JWS6 = generate_jws('hmac-based', Payload6, <<"abcdef">>),
% ClientInfo6 = ClientInfo#{password => JWS6},
% ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo6, ignored)),
%% Not Before
Payload7 = #{ <<"username">> => <<"myuser">>
, <<"nbf">> => erlang:system_time(second) - 60},
JWS7 = generate_jws('hmac-based', Payload7, <<"abcdef">>),
ClientInfo7 = ClientInfo#{password => JWS7},
?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo7, ignored)),
% %% Not Before
% Payload7 = #{ <<"username">> => <<"myuser">>
% , <<"nbf">> => erlang:system_time(second) - 60},
% JWS7 = generate_jws('hmac-based', Payload7, <<"abcdef">>),
% ClientInfo7 = ClientInfo#{password => JWS7},
% ?assertEqual({stop, {ok, #{is_superuser => false}}}, ?AUTH:authenticate(ClientInfo7, ignored)),
Payload8 = #{ <<"username">> => <<"myuser">>
, <<"nbf">> => erlang:system_time(second) + 60},
JWS8 = generate_jws('hmac-based', Payload8, <<"abcdef">>),
ClientInfo8 = ClientInfo#{password => JWS8},
?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo8, ignored)),
% Payload8 = #{ <<"username">> => <<"myuser">>
% , <<"nbf">> => erlang:system_time(second) + 60},
% JWS8 = generate_jws('hmac-based', Payload8, <<"abcdef">>),
% ClientInfo8 = ClientInfo#{password => JWS8},
% ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo8, ignored)),
?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID)),
ok.
% ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID)),
% ok.
t_jwt_authenticator2(_) ->
Dir = code:lib_dir(emqx_authn, test),
PublicKey = list_to_binary(filename:join([Dir, "data/public_key.pem"])),
PrivateKey = list_to_binary(filename:join([Dir, "data/private_key.pem"])),
AuthenticatorName = <<"myauthenticator">>,
Config = #{name => AuthenticatorName,
mechanism => jwt,
use_jwks => false,
algorithm => 'public-key',
certificate => PublicKey,
verify_claims => []},
{ok, #{name := AuthenticatorName, id := ID}} = ?AUTH:create_authenticator(?CHAIN, Config),
% t_jwt_authenticator2(_) ->
% Dir = code:lib_dir(emqx_authn, test),
% PublicKey = list_to_binary(filename:join([Dir, "data/public_key.pem"])),
% PrivateKey = list_to_binary(filename:join([Dir, "data/private_key.pem"])),
% AuthenticatorName = <<"myauthenticator">>,
% Config = #{name => AuthenticatorName,
% mechanism => jwt,
% use_jwks => false,
% algorithm => 'public-key',
% certificate => PublicKey,
% verify_claims => []},
% {ok, #{name := AuthenticatorName, id := ID}} = ?AUTH:create_authenticator(?CHAIN, Config),
Payload = #{<<"username">> => <<"myuser">>},
JWS = generate_jws('public-key', Payload, PrivateKey),
ClientInfo = #{username => <<"myuser">>,
password => JWS},
?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)),
?assertEqual({stop, {error, not_authorized}}, ?AUTH:authenticate(ClientInfo#{password => <<"badpassword">>}, ignored)),
% Payload = #{<<"username">> => <<"myuser">>},
% JWS = generate_jws('public-key', Payload, PrivateKey),
% ClientInfo = #{username => <<"myuser">>,
% password => JWS},
% ?assertEqual({stop, {ok, #{is_superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)),
% ?assertEqual({stop, {error, not_authorized}}, ?AUTH:authenticate(ClientInfo#{password => <<"badpassword">>}, ignored)),
?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID)),
ok.
% ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID)),
% ok.
generate_jws('hmac-based', Payload, Secret) ->
JWK = jose_jwk:from_oct(Secret),
Header = #{ <<"alg">> => <<"HS256">>
, <<"typ">> => <<"JWT">>
},
Signed = jose_jwt:sign(JWK, Header, Payload),
{_, JWS} = jose_jws:compact(Signed),
JWS;
generate_jws('public-key', Payload, PrivateKey) ->
JWK = jose_jwk:from_pem_file(PrivateKey),
Header = #{ <<"alg">> => <<"RS256">>
, <<"typ">> => <<"JWT">>
},
Signed = jose_jwt:sign(JWK, Header, Payload),
{_, JWS} = jose_jws:compact(Signed),
JWS.
% generate_jws('hmac-based', Payload, Secret) ->
% JWK = jose_jwk:from_oct(Secret),
% Header = #{ <<"alg">> => <<"HS256">>
% , <<"typ">> => <<"JWT">>
% },
% Signed = jose_jwt:sign(JWK, Header, Payload),
% {_, JWS} = jose_jws:compact(Signed),
% JWS;
% generate_jws('public-key', Payload, PrivateKey) ->
% JWK = jose_jwk:from_pem_file(PrivateKey),
% Header = #{ <<"alg">> => <<"RS256">>
% , <<"typ">> => <<"JWT">>
% },
% Signed = jose_jwt:sign(JWK, Header, Payload),
% {_, JWS} = jose_jws:compact(Signed),
% JWS.

View File

@ -19,146 +19,146 @@
-compile(export_all).
-compile(nowarn_export_all).
-include_lib("common_test/include/ct.hrl").
-include_lib("eunit/include/eunit.hrl").
% -include_lib("common_test/include/ct.hrl").
% -include_lib("eunit/include/eunit.hrl").
-include("emqx_authn.hrl").
% -include("emqx_authn.hrl").
-define(AUTH, emqx_authn).
% -define(AUTH, emqx_authn).
all() ->
emqx_ct:all(?MODULE).
init_per_suite(Config) ->
emqx_ct_helpers:start_apps([emqx_authn]),
Config.
% init_per_suite(Config) ->
% emqx_ct_helpers:start_apps([emqx_authn]),
% Config.
end_per_suite(_) ->
emqx_ct_helpers:stop_apps([emqx_authn]),
ok.
% end_per_suite(_) ->
% emqx_ct_helpers:stop_apps([emqx_authn]),
% ok.
t_mnesia_authenticator(_) ->
AuthenticatorName = <<"myauthenticator">>,
AuthenticatorConfig = #{name => AuthenticatorName,
mechanism => 'password-based',
server_type => 'built-in-database',
user_id_type => username,
password_hash_algorithm => #{
name => sha256
}},
{ok, #{name := AuthenticatorName, id := ID}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig),
% t_mnesia_authenticator(_) ->
% AuthenticatorName = <<"myauthenticator">>,
% AuthenticatorConfig = #{name => AuthenticatorName,
% mechanism => 'password-based',
% server_type => 'built-in-database',
% user_id_type => username,
% password_hash_algorithm => #{
% name => sha256
% }},
% {ok, #{name := AuthenticatorName, id := ID}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig),
UserInfo = #{user_id => <<"myuser">>,
password => <<"mypass">>},
?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:add_user(?CHAIN, ID, UserInfo)),
?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser">>)),
% UserInfo = #{user_id => <<"myuser">>,
% password => <<"mypass">>},
% ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:add_user(?CHAIN, ID, UserInfo)),
% ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser">>)),
ClientInfo = #{zone => external,
username => <<"myuser">>,
password => <<"mypass">>},
?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)),
?AUTH:enable(),
?assertEqual({ok, #{superuser => false}}, emqx_access_control:authenticate(ClientInfo)),
% ClientInfo = #{zone => external,
% username => <<"myuser">>,
% password => <<"mypass">>},
% ?assertEqual({stop, {ok, #{is_superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)),
% ?AUTH:enable(),
% ?assertEqual({ok, #{is_superuser => false}}, emqx_access_control:authenticate(ClientInfo)),
ClientInfo2 = ClientInfo#{username => <<"baduser">>},
?assertEqual({stop, {error, not_authorized}}, ?AUTH:authenticate(ClientInfo2, ignored)),
?assertEqual({error, not_authorized}, emqx_access_control:authenticate(ClientInfo2)),
% ClientInfo2 = ClientInfo#{username => <<"baduser">>},
% ?assertEqual({stop, {error, not_authorized}}, ?AUTH:authenticate(ClientInfo2, ignored)),
% ?assertEqual({error, not_authorized}, emqx_access_control:authenticate(ClientInfo2)),
ClientInfo3 = ClientInfo#{password => <<"badpass">>},
?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo3, ignored)),
?assertEqual({error, bad_username_or_password}, emqx_access_control:authenticate(ClientInfo3)),
% ClientInfo3 = ClientInfo#{password => <<"badpass">>},
% ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo3, ignored)),
% ?assertEqual({error, bad_username_or_password}, emqx_access_control:authenticate(ClientInfo3)),
UserInfo2 = UserInfo#{password => <<"mypass2">>},
?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:update_user(?CHAIN, ID, <<"myuser">>, UserInfo2)),
ClientInfo4 = ClientInfo#{password => <<"mypass2">>},
?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo4, ignored)),
% UserInfo2 = UserInfo#{password => <<"mypass2">>},
% ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:update_user(?CHAIN, ID, <<"myuser">>, UserInfo2)),
% ClientInfo4 = ClientInfo#{password => <<"mypass2">>},
% ?assertEqual({stop, {ok, #{is_superuser => false}}}, ?AUTH:authenticate(ClientInfo4, ignored)),
?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:update_user(?CHAIN, ID, <<"myuser">>, #{superuser => true})),
?assertEqual({stop, {ok, #{superuser => true}}}, ?AUTH:authenticate(ClientInfo4, ignored)),
% ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:update_user(?CHAIN, ID, <<"myuser">>, #{is_superuser => true})),
% ?assertEqual({stop, {ok, #{is_superuser => true}}}, ?AUTH:authenticate(ClientInfo4, ignored)),
?assertEqual(ok, ?AUTH:delete_user(?CHAIN, ID, <<"myuser">>)),
?assertEqual({error, not_found}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser">>)),
% ?assertEqual(ok, ?AUTH:delete_user(?CHAIN, ID, <<"myuser">>)),
% ?assertEqual({error, not_found}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser">>)),
?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:add_user(?CHAIN, ID, UserInfo)),
?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser">>)),
?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID)),
% ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:add_user(?CHAIN, ID, UserInfo)),
% ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser">>)),
% ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID)),
{ok, #{name := AuthenticatorName, id := ID1}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig),
?assertMatch({error, not_found}, ?AUTH:lookup_user(?CHAIN, ID1, <<"myuser">>)),
?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID1)),
ok.
% {ok, #{name := AuthenticatorName, id := ID1}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig),
% ?assertMatch({error, not_found}, ?AUTH:lookup_user(?CHAIN, ID1, <<"myuser">>)),
% ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID1)),
% ok.
t_import(_) ->
AuthenticatorName = <<"myauthenticator">>,
AuthenticatorConfig = #{name => AuthenticatorName,
mechanism => 'password-based',
server_type => 'built-in-database',
user_id_type => username,
password_hash_algorithm => #{
name => sha256
}},
{ok, #{name := AuthenticatorName, id := ID}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig),
% t_import(_) ->
% AuthenticatorName = <<"myauthenticator">>,
% AuthenticatorConfig = #{name => AuthenticatorName,
% mechanism => 'password-based',
% server_type => 'built-in-database',
% user_id_type => username,
% password_hash_algorithm => #{
% name => sha256
% }},
% {ok, #{name := AuthenticatorName, id := ID}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig),
Dir = code:lib_dir(emqx_authn, test),
?assertEqual(ok, ?AUTH:import_users(?CHAIN, ID, filename:join([Dir, "data/user-credentials.json"]))),
?assertEqual(ok, ?AUTH:import_users(?CHAIN, ID, filename:join([Dir, "data/user-credentials.csv"]))),
?assertMatch({ok, #{user_id := <<"myuser1">>}}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser1">>)),
?assertMatch({ok, #{user_id := <<"myuser3">>}}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser3">>)),
% Dir = code:lib_dir(emqx_authn, test),
% ?assertEqual(ok, ?AUTH:import_users(?CHAIN, ID, filename:join([Dir, "data/user-credentials.json"]))),
% ?assertEqual(ok, ?AUTH:import_users(?CHAIN, ID, filename:join([Dir, "data/user-credentials.csv"]))),
% ?assertMatch({ok, #{user_id := <<"myuser1">>}}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser1">>)),
% ?assertMatch({ok, #{user_id := <<"myuser3">>}}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser3">>)),
ClientInfo1 = #{username => <<"myuser1">>,
password => <<"mypassword1">>},
?assertEqual({stop, {ok, #{superuser => true}}}, ?AUTH:authenticate(ClientInfo1, ignored)),
% ClientInfo1 = #{username => <<"myuser1">>,
% password => <<"mypassword1">>},
% ?assertEqual({stop, {ok, #{is_superuser => true}}}, ?AUTH:authenticate(ClientInfo1, ignored)),
ClientInfo2 = ClientInfo1#{username => <<"myuser2">>,
password => <<"mypassword2">>},
?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo2, ignored)),
% ClientInfo2 = ClientInfo1#{username => <<"myuser2">>,
% password => <<"mypassword2">>},
% ?assertEqual({stop, {ok, #{is_superuser => false}}}, ?AUTH:authenticate(ClientInfo2, ignored)),
ClientInfo3 = ClientInfo1#{username => <<"myuser3">>,
password => <<"mypassword3">>},
?assertEqual({stop, {ok, #{superuser => true}}}, ?AUTH:authenticate(ClientInfo3, ignored)),
% ClientInfo3 = ClientInfo1#{username => <<"myuser3">>,
% password => <<"mypassword3">>},
% ?assertEqual({stop, {ok, #{is_superuser => true}}}, ?AUTH:authenticate(ClientInfo3, ignored)),
?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID)),
ok.
% ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID)),
% ok.
t_multi_mnesia_authenticator(_) ->
AuthenticatorName1 = <<"myauthenticator1">>,
AuthenticatorConfig1 = #{name => AuthenticatorName1,
mechanism => 'password-based',
server_type => 'built-in-database',
user_id_type => username,
password_hash_algorithm => #{
name => sha256
}},
AuthenticatorName2 = <<"myauthenticator2">>,
AuthenticatorConfig2 = #{name => AuthenticatorName2,
mechanism => 'password-based',
server_type => 'built-in-database',
user_id_type => clientid,
password_hash_algorithm => #{
name => sha256
}},
{ok, #{name := AuthenticatorName1, id := ID1}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig1),
{ok, #{name := AuthenticatorName2, id := ID2}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig2),
% t_multi_mnesia_authenticator(_) ->
% AuthenticatorName1 = <<"myauthenticator1">>,
% AuthenticatorConfig1 = #{name => AuthenticatorName1,
% mechanism => 'password-based',
% server_type => 'built-in-database',
% user_id_type => username,
% password_hash_algorithm => #{
% name => sha256
% }},
% AuthenticatorName2 = <<"myauthenticator2">>,
% AuthenticatorConfig2 = #{name => AuthenticatorName2,
% mechanism => 'password-based',
% server_type => 'built-in-database',
% user_id_type => clientid,
% password_hash_algorithm => #{
% name => sha256
% }},
% {ok, #{name := AuthenticatorName1, id := ID1}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig1),
% {ok, #{name := AuthenticatorName2, id := ID2}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig2),
?assertMatch({ok, #{user_id := <<"myuser">>}},
?AUTH:add_user(?CHAIN, ID1,
#{user_id => <<"myuser">>,
password => <<"mypass1">>})),
?assertMatch({ok, #{user_id := <<"myclient">>}},
?AUTH:add_user(?CHAIN, ID2,
#{user_id => <<"myclient">>,
password => <<"mypass2">>})),
% ?assertMatch({ok, #{user_id := <<"myuser">>}},
% ?AUTH:add_user(?CHAIN, ID1,
% #{user_id => <<"myuser">>,
% password => <<"mypass1">>})),
% ?assertMatch({ok, #{user_id := <<"myclient">>}},
% ?AUTH:add_user(?CHAIN, ID2,
% #{user_id => <<"myclient">>,
% password => <<"mypass2">>})),
ClientInfo1 = #{username => <<"myuser">>,
clientid => <<"myclient">>,
password => <<"mypass1">>},
?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo1, ignored)),
?assertEqual(ok, ?AUTH:move_authenticator(?CHAIN, ID2, top)),
% ClientInfo1 = #{username => <<"myuser">>,
% clientid => <<"myclient">>,
% password => <<"mypass1">>},
% ?assertEqual({stop, {ok, #{is_superuser => false}}}, ?AUTH:authenticate(ClientInfo1, ignored)),
% ?assertEqual(ok, ?AUTH:move_authenticator(?CHAIN, ID2, top)),
?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo1, ignored)),
ClientInfo2 = ClientInfo1#{password => <<"mypass2">>},
?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo2, ignored)),
% ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo1, ignored)),
% ClientInfo2 = ClientInfo1#{password => <<"mypass2">>},
% ?assertEqual({stop, {ok, #{is_superuser => false}}}, ?AUTH:authenticate(ClientInfo2, ignored)),
?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID1)),
?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID2)),
ok.
% ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID1)),
% ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID2)),
% ok.

View File

@ -2,17 +2,14 @@ authorization {
sources = [
# {
# type: http
# config: {
# url: "https://emqx.com"
# headers: {
# Accept: "application/json"
# Content-Type: "application/json"
# }
# }
# },
# {
# type: mysql
# config: {
# server: "127.0.0.1:3306"
# database: mqtt
# pool_size: 1
@ -25,12 +22,10 @@ authorization {
# certfile: "{{ platform_etc_dir }}/certs/client-cert.pem"
# keyfile: "{{ platform_etc_dir }}/certs/client-key.pem"
# }
# }
# sql: "select ipaddress, username, clientid, action, permission, topic from mqtt_authz where ipaddr = '%a' or username = '%u' or clientid = '%c'"
# },
# {
# type: pgsql
# config: {
# server: "127.0.0.1:5432"
# database: mqtt
# pool_size: 1
@ -38,36 +33,31 @@ authorization {
# password: public
# auto_reconnect: true
# ssl: {enable: false}
# }
# sql: "select ipaddress, username, clientid, action, permission, topic from mqtt_authz where ipaddr = '%a' or username = '%u' or username = '$all' or clientid = '%c'"
# },
# {
# type: redis
# config: {
# server: "127.0.0.1:6379"
# database: 0
# pool_size: 1
# password: public
# auto_reconnect: true
# ssl: {enable: false}
# }
# cmd: "HGETALL mqtt_authz:%u"
# },
# {
# type: mongo
# config: {
# mongo_type: single
# server: "127.0.0.1:27017"
# pool_size: 1
# database: mqtt
# ssl: {enable: false}
# }
# collection: mqtt_authz
# find: { "$or": [ { "username": "%u" }, { "clientid": "%c" } ] }
# },
{
type: file
path: "{{ platform_etc_dir }}/authorization_rules.conf"
path: "{{ platform_etc_dir }}/acl.conf"
}
]
}

View File

@ -30,13 +30,16 @@
, lookup/0
, lookup/1
, move/2
, move/3
, update/2
, update/3
, authorize/5
]).
-export([post_config_update/4, pre_config_update/2]).
-define(CONF_KEY_PATH, [authorization, sources]).
-define(SOURCE_TYPES, [file, http, mongo, mysql, pgsql, redis]).
-spec(register_metrics() -> ok).
register_metrics() ->
@ -45,88 +48,123 @@ register_metrics() ->
init() ->
ok = register_metrics(),
emqx_config_handler:add_handler(?CONF_KEY_PATH, ?MODULE),
NSources = [init_source(Source) || Source <- emqx:get_config(?CONF_KEY_PATH, [])],
Sources = emqx:get_config(?CONF_KEY_PATH, []),
ok = check_dup_types(Sources),
NSources = [init_source(Source) || Source <- Sources],
ok = emqx_hooks:add('client.authorize', {?MODULE, authorize, [NSources]}, -1).
lookup() ->
{_M, _F, [A]}= find_action_in_hooks(),
A.
lookup(Id) ->
try find_source_by_id(Id, lookup()) of
lookup(Type) ->
try find_source_by_type(atom(Type), lookup()) of
{_, Source} -> Source
catch
error:Reason -> {error, Reason}
end.
move(Id, Position) ->
emqx:update_config(?CONF_KEY_PATH, {move, Id, Position}).
move(Type, Cmd) ->
move(Type, Cmd, #{}).
move(Type, #{<<"before">> := Before}, Opts) ->
emqx:update_config(?CONF_KEY_PATH, {move, atom(Type), #{<<"before">> => atom(Before)}}, Opts);
move(Type, #{<<"after">> := After}, Opts) ->
emqx:update_config(?CONF_KEY_PATH, {move, atom(Type), #{<<"after">> => atom(After)}}, Opts);
move(Type, Position, Opts) ->
emqx:update_config(?CONF_KEY_PATH, {move, atom(Type), Position}, Opts).
update(Cmd, Sources) ->
emqx:update_config(?CONF_KEY_PATH, {Cmd, Sources}).
update(Cmd, Sources, #{}).
pre_config_update({move, Id, <<"top">>}, Conf) when is_list(Conf) ->
{Index, _} = find_source_by_id(Id),
update({replace_once, Type}, Sources, Opts) ->
emqx:update_config(?CONF_KEY_PATH, {{replace_once, atom(Type)}, Sources}, Opts);
update({delete_once, Type}, Sources, Opts) ->
emqx:update_config(?CONF_KEY_PATH, {{delete_once, atom(Type)}, Sources}, Opts);
update(Cmd, Sources, Opts) ->
emqx:update_config(?CONF_KEY_PATH, {Cmd, Sources}, Opts).
pre_config_update({move, Type, <<"top">>}, Conf) when is_list(Conf) ->
{Index, _} = find_source_by_type(Type),
{List1, List2} = lists:split(Index, Conf),
{ok, [lists:nth(Index, Conf)] ++ lists:droplast(List1) ++ List2};
NConf = [lists:nth(Index, Conf)] ++ lists:droplast(List1) ++ List2,
ok = check_dup_types(NConf),
{ok, NConf};
pre_config_update({move, Id, <<"bottom">>}, Conf) when is_list(Conf) ->
{Index, _} = find_source_by_id(Id),
pre_config_update({move, Type, <<"bottom">>}, Conf) when is_list(Conf) ->
{Index, _} = find_source_by_type(Type),
{List1, List2} = lists:split(Index, Conf),
{ok, lists:droplast(List1) ++ List2 ++ [lists:nth(Index, Conf)]};
NConf = lists:droplast(List1) ++ List2 ++ [lists:nth(Index, Conf)],
ok = check_dup_types(NConf),
{ok, NConf};
pre_config_update({move, Id, #{<<"before">> := BeforeId}}, Conf) when is_list(Conf) ->
{Index1, _} = find_source_by_id(Id),
pre_config_update({move, Type, #{<<"before">> := Before}}, Conf) when is_list(Conf) ->
{Index1, _} = find_source_by_type(Type),
Conf1 = lists:nth(Index1, Conf),
{Index2, _} = find_source_by_id(BeforeId),
{Index2, _} = find_source_by_type(Before),
Conf2 = lists:nth(Index2, Conf),
{List1, List2} = lists:split(Index2, Conf),
{ok, lists:delete(Conf1, lists:droplast(List1))
NConf = lists:delete(Conf1, lists:droplast(List1))
++ [Conf1] ++ [Conf2]
++ lists:delete(Conf1, List2)};
++ lists:delete(Conf1, List2),
ok = check_dup_types(NConf),
{ok, NConf};
pre_config_update({move, Id, #{<<"after">> := AfterId}}, Conf) when is_list(Conf) ->
{Index1, _} = find_source_by_id(Id),
pre_config_update({move, Type, #{<<"after">> := After}}, Conf) when is_list(Conf) ->
{Index1, _} = find_source_by_type(Type),
Conf1 = lists:nth(Index1, Conf),
{Index2, _} = find_source_by_id(AfterId),
{Index2, _} = find_source_by_type(After),
{List1, List2} = lists:split(Index2, Conf),
{ok, lists:delete(Conf1, List1)
NConf = lists:delete(Conf1, List1)
++ [Conf1]
++ lists:delete(Conf1, List2)};
++ lists:delete(Conf1, List2),
ok = check_dup_types(NConf),
{ok, NConf};
pre_config_update({head, Sources}, Conf) when is_list(Sources), is_list(Conf) ->
NConf = Sources ++ Conf,
ok = check_dup_types(NConf),
{ok, Sources ++ Conf};
pre_config_update({tail, Sources}, Conf) when is_list(Sources), is_list(Conf) ->
NConf = Conf ++ Sources,
ok = check_dup_types(NConf),
{ok, Conf ++ Sources};
pre_config_update({{replace_once, Id}, Source}, Conf) when is_map(Source), is_list(Conf) ->
{Index, _} = find_source_by_id(Id),
pre_config_update({{replace_once, Type}, Source}, Conf) when is_map(Source), is_list(Conf) ->
{Index, _} = find_source_by_type(Type),
{List1, List2} = lists:split(Index, Conf),
{ok, lists:droplast(List1) ++ [Source] ++ List2};
NConf = lists:droplast(List1) ++ [Source] ++ List2,
ok = check_dup_types(NConf),
{ok, NConf};
pre_config_update({{delete_once, Type}, _Source}, Conf) when is_list(Conf) ->
{_, Source} = find_source_by_type(Type),
NConf = lists:delete(Source, Conf),
ok = check_dup_types(NConf),
{ok, NConf};
pre_config_update({_, Sources}, _Conf) when is_list(Sources)->
%% overwrite the entire config!
{ok, Sources}.
post_config_update(_, undefined, _Conf, _AppEnvs) ->
ok;
post_config_update({move, Id, <<"top">>}, _NewSources, _OldSources, _AppEnvs) ->
post_config_update({move, Type, <<"top">>}, _NewSources, _OldSources, _AppEnvs) ->
InitedSources = lookup(),
{Index, Source} = find_source_by_id(Id, InitedSources),
{Index, Source} = find_source_by_type(Type, InitedSources),
{Sources1, Sources2 } = lists:split(Index, InitedSources),
Sources3 = [Source] ++ lists:droplast(Sources1) ++ Sources2,
ok = emqx_hooks:put('client.authorize', {?MODULE, authorize, [Sources3]}, -1),
ok = emqx_authz_cache:drain_cache();
post_config_update({move, Id, <<"bottom">>}, _NewSources, _OldSources, _AppEnvs) ->
post_config_update({move, Type, <<"bottom">>}, _NewSources, _OldSources, _AppEnvs) ->
InitedSources = lookup(),
{Index, Source} = find_source_by_id(Id, InitedSources),
{Index, Source} = find_source_by_type(Type, InitedSources),
{Sources1, Sources2 } = lists:split(Index, InitedSources),
Sources3 = lists:droplast(Sources1) ++ Sources2 ++ [Source],
ok = emqx_hooks:put('client.authorize', {?MODULE, authorize, [Sources3]}, -1),
ok = emqx_authz_cache:drain_cache();
post_config_update({move, Id, #{<<"before">> := BeforeId}}, _NewSources, _OldSources, _AppEnvs) ->
post_config_update({move, Type, #{<<"before">> := Before}}, _NewSources, _OldSources, _AppEnvs) ->
InitedSources = lookup(),
{_, Source0} = find_source_by_id(Id, InitedSources),
{Index, Source1} = find_source_by_id(BeforeId, InitedSources),
{_, Source0} = find_source_by_type(Type, InitedSources),
{Index, Source1} = find_source_by_type(Before, InitedSources),
{Sources1, Sources2} = lists:split(Index, InitedSources),
Sources3 = lists:delete(Source0, lists:droplast(Sources1))
++ [Source0] ++ [Source1]
@ -134,10 +172,10 @@ post_config_update({move, Id, #{<<"before">> := BeforeId}}, _NewSources, _OldSou
ok = emqx_hooks:put('client.authorize', {?MODULE, authorize, [Sources3]}, -1),
ok = emqx_authz_cache:drain_cache();
post_config_update({move, Id, #{<<"after">> := AfterId}}, _NewSources, _OldSources, _AppEnvs) ->
post_config_update({move, Type, #{<<"after">> := After}}, _NewSources, _OldSources, _AppEnvs) ->
InitedSources = lookup(),
{_, Source} = find_source_by_id(Id, InitedSources),
{Index, _} = find_source_by_id(AfterId, InitedSources),
{_, Source} = find_source_by_type(Type, InitedSources),
{Index, _} = find_source_by_type(After, InitedSources),
{Sources1, Sources2} = lists:split(Index, InitedSources),
Sources3 = lists:delete(Source, Sources1)
++ [Source]
@ -155,20 +193,30 @@ post_config_update({tail, Sources}, _NewSources, _OldConf, _AppEnvs) ->
emqx_hooks:put('client.authorize', {?MODULE, authorize, [lookup() ++ InitedSources]}, -1),
ok = emqx_authz_cache:drain_cache();
post_config_update({{replace_once, Id}, Source}, _NewSources, _OldConf, _AppEnvs) when is_map(Source) ->
post_config_update({{replace_once, Type}, #{type := Type} = Source}, _NewSources, _OldConf, _AppEnvs) when is_map(Source) ->
OldInitedSources = lookup(),
{Index, OldSource} = find_source_by_id(Id, OldInitedSources),
{Index, OldSource} = find_source_by_type(Type, OldInitedSources),
case maps:get(type, OldSource, undefined) of
undefined -> ok;
file -> ok;
_ ->
#{annotations := #{id := Id}} = OldSource,
ok = emqx_resource:remove(Id)
end,
{OldSources1, OldSources2 } = lists:split(Index, OldInitedSources),
InitedSources = [init_source(R#{annotations => #{id => Id}}) || R <- check_sources([Source])],
InitedSources = [init_source(R) || R <- check_sources([Source])],
ok = emqx_hooks:put('client.authorize', {?MODULE, authorize, [lists:droplast(OldSources1) ++ InitedSources ++ OldSources2]}, -1),
ok = emqx_authz_cache:drain_cache();
post_config_update({{delete_once, Type}, _Source}, _NewSources, _OldConf, _AppEnvs) ->
OldInitedSources = lookup(),
{_, OldSource} = find_source_by_type(Type, OldInitedSources),
case OldSource of
#{annotations := #{id := Id}} ->
ok = emqx_resource:remove(Id);
_ -> ok
end,
ok = emqx_hooks:put('client.authorize', {?MODULE, authorize, [lists:delete(OldSource, OldInitedSources)]}, -1),
ok = emqx_authz_cache:drain_cache();
post_config_update(_, NewSources, _OldConf, _AppEnvs) ->
%% overwrite the entire config!
OldInitedSources = lookup(),
@ -181,46 +229,28 @@ post_config_update(_, NewSources, _OldConf, _AppEnvs) ->
ok = emqx_authz_cache:drain_cache().
%%--------------------------------------------------------------------
%% Internal functions
%% Initialize source
%%--------------------------------------------------------------------
check_sources(RawSources) ->
{ok, Conf} = hocon:binary(jsx:encode(#{<<"authorization">> => #{<<"sources">> => RawSources}}), #{format => richmap}),
CheckConf = hocon_schema:check(emqx_authz_schema, Conf, #{atom_key => true}),
#{authorization:= #{sources := Sources}} = hocon_schema:richmap_to_map(CheckConf),
Sources.
find_source_by_id(Id) -> find_source_by_id(Id, lookup()).
find_source_by_id(Id, Sources) -> find_source_by_id(Id, Sources, 1).
find_source_by_id(_SourceId, [], _N) -> error(not_found_rule);
find_source_by_id(SourceId, [ Source = #{annotations := #{id := Id}} | Tail], N) ->
case SourceId =:= Id of
true -> {N, Source};
false -> find_source_by_id(SourceId, Tail, N + 1)
end.
find_action_in_hooks() ->
Callbacks = emqx_hooks:lookup('client.authorize'),
[Action] = [Action || {callback,{?MODULE, authorize, _} = Action, _, _} <- Callbacks ],
Action.
gen_id(Type) ->
iolist_to_binary([io_lib:format("~s_~s",[?APP, Type]), "_", integer_to_list(erlang:system_time())]).
create_resource(#{type := DB,
config := Config,
annotations := #{id := ResourceID}}) ->
case emqx_resource:update(ResourceID, connector_module(DB), Config, []) of
{ok, _} -> ResourceID;
{error, Reason} -> {error, Reason}
check_dup_types(Sources) ->
check_dup_types(Sources, ?SOURCE_TYPES).
check_dup_types(_Sources, []) -> ok;
check_dup_types(Sources, [T0 | Tail]) ->
case lists:foldl(fun (#{type := T1}, AccIn) ->
case T0 =:= T1 of
true -> AccIn + 1;
false -> AccIn
end;
create_resource(#{type := DB,
config := Config}) ->
ResourceID = gen_id(DB),
case emqx_resource:create(ResourceID, connector_module(DB), Config) of
{ok, already_created} -> ResourceID;
{ok, _} -> ResourceID;
{error, Reason} -> {error, Reason}
(#{<<"type">> := T1}, AccIn) ->
case T0 =:= atom(T1) of
true -> AccIn + 1;
false -> AccIn
end
end, 0, Sources) > 1 of
true ->
?LOG(error, "The type is duplicated in the Authorization source"),
{error, authz_source_dup};
false -> check_dup_types(Sources, Tail)
end.
init_source(#{enable := true,
@ -240,20 +270,15 @@ init_source(#{enable := true,
?LOG(alert, "Failed to read ~s: ~p", [Path, Reason]),
error(Reason)
end,
Source#{annotations =>
#{id => gen_id(file),
rules => Rules
}};
Source#{annotations => #{rules => Rules}};
init_source(#{enable := true,
type := http,
config := #{url := Url} = Config
url := Url
} = Source) ->
NConfig = maps:merge(Config, #{base_url => maps:remove(query, Url)}),
case create_resource(Source#{config := NConfig}) of
NSource= maps:put(base_url, maps:remove(query, Url), Source),
case create_resource(NSource) of
{error, Reason} -> error({load_config_error, Reason});
Id -> Source#{annotations =>
#{id => Id}
}
Id -> Source#{annotations => #{id => Id}}
end;
init_source(#{enable := true,
type := DB
@ -261,9 +286,7 @@ init_source(#{enable := true,
DB =:= mongo ->
case create_resource(Source) of
{error, Reason} -> error({load_config_error, Reason});
Id -> Source#{annotations =>
#{id => Id}
}
Id -> Source#{annotations => #{id => Id}}
end;
init_source(#{enable := true,
type := DB,
@ -323,8 +346,56 @@ do_authorize(Client, PubSub, Topic,
Matched -> Matched
end.
%%--------------------------------------------------------------------
%% Internal function
%%--------------------------------------------------------------------
check_sources(RawSources) ->
Schema = #{roots => emqx_authz_schema:fields("authorization"), fields => #{}},
Conf = #{<<"sources">> => RawSources},
#{sources := Sources} = hocon_schema:check_plain(Schema, Conf, #{atom_key => true}),
Sources.
find_source_by_type(Type) -> find_source_by_type(Type, lookup()).
find_source_by_type(Type, Sources) -> find_source_by_type(Type, Sources, 1).
find_source_by_type(_, [], _N) -> error(not_found_source);
find_source_by_type(Type, [ Source = #{type := T} | Tail], N) ->
case Type =:= T of
true -> {N, Source};
false -> find_source_by_type(Type, Tail, N + 1)
end.
find_action_in_hooks() ->
Callbacks = emqx_hooks:lookup('client.authorize'),
[Action] = [Action || {callback,{?MODULE, authorize, _} = Action, _, _} <- Callbacks ],
Action.
gen_id(Type) ->
iolist_to_binary([io_lib:format("~s_~s",[?APP, Type])]).
create_resource(#{type := DB,
annotations := #{id := ResourceID}} = Source) ->
case emqx_resource:update(ResourceID, connector_module(DB), Source, []) of
{ok, _} -> ResourceID;
{error, Reason} -> {error, Reason}
end;
create_resource(#{type := DB} = Source) ->
ResourceID = gen_id(DB),
case emqx_resource:create(ResourceID, connector_module(DB), Source) of
{ok, already_created} -> ResourceID;
{ok, _} -> ResourceID;
{error, Reason} -> {error, Reason}
end.
authz_module(Type) ->
list_to_existing_atom("emqx_authz_" ++ atom_to_list(Type)).
connector_module(Type) ->
list_to_existing_atom("emqx_connector_" ++ atom_to_list(Type)).
atom(B) when is_binary(B) ->
try binary_to_existing_atom(B, utf8)
catch
_ -> binary_to_atom(B)
end;
atom(A) when is_atom(A) -> A.

View File

@ -1,528 +0,0 @@
%%--------------------------------------------------------------------
%% 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).
-behavior(minirest_api).
-include("emqx_authz.hrl").
-define(EXAMPLE_RETURNED_RULE1,
#{principal => <<"all">>,
permission => <<"allow">>,
action => <<"all">>,
topics => [<<"#">>],
annotations => #{id => 1}
}).
-define(EXAMPLE_RETURNED_RULES,
#{rules => [?EXAMPLE_RETURNED_RULE1
]
}).
-define(EXAMPLE_RULE1, #{principal => <<"all">>,
permission => <<"allow">>,
action => <<"all">>,
topics => [<<"#">>]}).
-export([ api_spec/0
, rules/2
, rule/2
, move_rule/2
]).
api_spec() ->
{[ rules_api()
, rule_api()
, move_rule_api()
], definitions()}.
definitions() -> emqx_authz_api_schema:definitions().
rules_api() ->
Metadata = #{
get => #{
description => "List authorization rules",
parameters => [
#{
name => page,
in => query,
schema => #{
type => integer
},
required => false
},
#{
name => limit,
in => query,
schema => #{
type => integer
},
required => false
}
],
responses => #{
<<"200">> => #{
description => <<"OK">>,
content => #{
'application/json' => #{
schema => #{
type => object,
required => [rules],
properties => #{rules => #{
type => array,
items => minirest:ref(<<"returned_rules">>)
}
}
},
examples => #{
rules => #{
summary => <<"Rules">>,
value => jsx:encode(?EXAMPLE_RETURNED_RULES)
}
}
}
}
}
}
},
post => #{
description => "Add new rule",
requestBody => #{
content => #{
'application/json' => #{
schema => minirest:ref(<<"rules">>),
examples => #{
simple_rule => #{
summary => <<"Rules">>,
value => jsx:encode(?EXAMPLE_RULE1)
}
}
}
}
},
responses => #{
<<"204">> => #{description => <<"Created">>},
<<"400">> => #{
description => <<"Bad Request">>,
content => #{
'application/json' => #{
schema => minirest:ref(<<"error">>),
examples => #{
example1 => #{
summary => <<"Bad Request">>,
value => #{
code => <<"BAD_REQUEST">>,
message => <<"Bad Request">>
}
}
}
}
}
}
}
},
put => #{
description => "Update all rules",
requestBody => #{
content => #{
'application/json' => #{
schema => #{
type => array,
items => minirest:ref(<<"returned_rules">>)
},
examples => #{
rules => #{
summary => <<"Rules">>,
value => jsx:encode([?EXAMPLE_RULE1])
}
}
}
}
},
responses => #{
<<"204">> => #{description => <<"Created">>},
<<"400">> => #{
description => <<"Bad Request">>,
content => #{
'application/json' => #{
schema => minirest:ref(<<"error">>),
examples => #{
example1 => #{
summary => <<"Bad Request">>,
value => #{
code => <<"BAD_REQUEST">>,
message => <<"Bad Request">>
}
}
}
}
}
}
}
}
},
{"/authorization", Metadata, rules}.
rule_api() ->
Metadata = #{
get => #{
description => "List authorization rules",
parameters => [
#{
name => id,
in => path,
schema => #{
type => string
},
required => true
}
],
responses => #{
<<"200">> => #{
description => <<"OK">>,
content => #{
'application/json' => #{
schema => minirest:ref(<<"returned_rules">>),
examples => #{
rules => #{
summary => <<"Rules">>,
value => jsx:encode(?EXAMPLE_RETURNED_RULE1)
}
}
}
}
},
<<"404">> => #{
description => <<"Bad Request">>,
content => #{
'application/json' => #{
schema => minirest:ref(<<"error">>),
examples => #{
example1 => #{
summary => <<"Not Found">>,
value => #{
code => <<"NOT_FOUND">>,
message => <<"rule xxx not found">>
}
}
}
}
}
}
}
},
put => #{
description => "Update rule",
parameters => [
#{
name => id,
in => path,
schema => #{
type => string
},
required => true
}
],
requestBody => #{
content => #{
'application/json' => #{
schema => minirest:ref(<<"rules">>),
examples => #{
simple_rule => #{
summary => <<"Rules">>,
value => jsx:encode(?EXAMPLE_RULE1)
}
}
}
}
},
responses => #{
<<"204">> => #{description => <<"No Content">>},
<<"404">> => #{
description => <<"Bad Request">>,
content => #{
'application/json' => #{
schema => minirest:ref(<<"error">>),
examples => #{
example1 => #{
summary => <<"Not Found">>,
value => #{
code => <<"NOT_FOUND">>,
message => <<"rule xxx not found">>
}
}
}
}
}
},
<<"400">> => #{
description => <<"Bad Request">>,
content => #{
'application/json' => #{
schema => minirest:ref(<<"error">>),
examples => #{
example1 => #{
summary => <<"Bad Request">>,
value => #{
code => <<"BAD_REQUEST">>,
message => <<"Bad Request">>
}
}
}
}
}
}
}
},
delete => #{
description => "Delete rule",
parameters => [
#{
name => id,
in => path,
schema => #{
type => string
},
required => true
}
],
responses => #{
<<"204">> => #{description => <<"No Content">>},
<<"400">> => #{
description => <<"Bad Request">>,
content => #{
'application/json' => #{
schema => minirest:ref(<<"error">>),
examples => #{
example1 => #{
summary => <<"Bad Request">>,
value => #{
code => <<"BAD_REQUEST">>,
message => <<"Bad Request">>
}
}
}
}
}
}
}
}
},
{"/authorization/:id", Metadata, rule}.
move_rule_api() ->
Metadata = #{
post => #{
description => "Change the order of rules",
parameters => [
#{
name => id,
in => path,
schema => #{
type => string
},
required => true
}
],
requestBody => #{
content => #{
'application/json' => #{
schema => #{
type => object,
required => [position],
properties => #{
position => #{
oneOf => [
#{type => string,
enum => [<<"top">>, <<"bottom">>]
},
#{type => object,
required => ['after'],
properties => #{
'after' => #{
type => string
}
}
},
#{type => object,
required => ['before'],
properties => #{
'before' => #{
type => string
}
}
}
]
}
}
}
}
}
},
responses => #{
<<"204">> => #{
description => <<"No Content">>
},
<<"404">> => #{
description => <<"Bad Request">>,
content => #{
'application/json' => #{
schema => minirest:ref(<<"error">>),
examples => #{
example1 => #{
summary => <<"Not Found">>,
value => #{
code => <<"NOT_FOUND">>,
message => <<"rule xxx not found">>
}
}
}
}
}
},
<<"400">> => #{
description => <<"Bad Request">>,
content => #{
'application/json' => #{
schema => minirest:ref(<<"error">>),
examples => #{
example1 => #{
summary => <<"Bad Request">>,
value => #{
code => <<"BAD_REQUEST">>,
message => <<"Bad Request">>
}
}
}
}
}
}
}
}
},
{"/authorization/:id/move", Metadata, move_rule}.
rules(get, #{query_string := Query}) ->
Rules = lists:foldl(fun (#{type := _Type, enable := true, config := #{server := Server} = Config, annotations := #{id := Id}} = Rule, AccIn) ->
NRule = case emqx_resource:health_check(Id) of
ok ->
Rule#{config => Config#{server => emqx_connector_schema_lib:ip_port_to_string(Server)},
annotations => #{id => Id,
status => healthy}};
_ ->
Rule#{config => Config#{server => emqx_connector_schema_lib:ip_port_to_string(Server)},
annotations => #{id => Id,
status => unhealthy}}
end,
lists:append(AccIn, [NRule]);
(#{type := _Type, enable := true, annotations := #{id := Id}} = Rule, AccIn) ->
NRule = case emqx_resource:health_check(Id) of
ok ->
Rule#{annotations => #{id => Id,
status => healthy}};
_ ->
Rule#{annotations => #{id => Id,
status => unhealthy}}
end,
lists:append(AccIn, [NRule]);
(Rule, AccIn) ->
lists:append(AccIn, [Rule])
end, [], emqx_authz:lookup()),
case maps:is_key(<<"page">>, Query) andalso maps:is_key(<<"limit">>, Query) of
true ->
Page = maps:get(<<"page">>, Query),
Limit = maps:get(<<"limit">>, Query),
Index = (binary_to_integer(Page) - 1) * binary_to_integer(Limit),
{_, Rules1} = lists:split(Index, Rules),
case binary_to_integer(Limit) < length(Rules1) of
true ->
{Rules2, _} = lists:split(binary_to_integer(Limit), Rules1),
{200, #{rules => Rules2}};
false -> {200, #{rules => Rules1}}
end;
false -> {200, #{rules => Rules}}
end;
rules(post, #{body := RawConfig}) ->
case emqx_authz:update(head, [RawConfig]) of
{ok, _} -> {204};
{error, Reason} ->
{400, #{code => <<"BAD_REQUEST">>,
messgae => atom_to_binary(Reason)}}
end;
rules(put, #{body := RawConfig}) ->
case emqx_authz:update(replace, RawConfig) of
{ok, _} -> {204};
{error, Reason} ->
{400, #{code => <<"BAD_REQUEST">>,
messgae => atom_to_binary(Reason)}}
end.
rule(get, #{bindings := #{id := Id}}) ->
case emqx_authz:lookup(Id) of
{error, Reason} -> {404, #{messgae => atom_to_binary(Reason)}};
#{type := file} = Rule -> {200, Rule};
#{config := #{server := Server} = Config} = Rule ->
case emqx_resource:health_check(Id) of
ok ->
{200, Rule#{config => Config#{server => emqx_connector_schema_lib:ip_port_to_string(Server)},
annotations => #{id => Id,
status => healthy}}};
_ ->
{200, Rule#{config => Config#{server => emqx_connector_schema_lib:ip_port_to_string(Server)},
annotations => #{id => Id,
status => unhealthy}}}
end;
Rule ->
case emqx_resource:health_check(Id) of
ok ->
{200, Rule#{annotations => #{id => Id,
status => healthy}}};
_ ->
{200, Rule#{annotations => #{id => Id,
status => unhealthy}}}
end
end;
rule(put, #{bindings := #{id := RuleId}, body := RawConfig}) ->
case emqx_authz:update({replace_once, RuleId}, RawConfig) of
{ok, _} -> {204};
{error, not_found_rule} ->
{404, #{code => <<"NOT_FOUND">>,
messgae => <<"rule ", RuleId/binary, " not found">>}};
{error, Reason} ->
{400, #{code => <<"BAD_REQUEST">>,
messgae => atom_to_binary(Reason)}}
end;
rule(delete, #{bindings := #{id := RuleId}}) ->
case emqx_authz:update({replace_once, RuleId}, #{}) of
{ok, _} -> {204};
{error, Reason} ->
{400, #{code => <<"BAD_REQUEST">>,
messgae => atom_to_binary(Reason)}}
end.
move_rule(post, #{bindings := #{id := RuleId}, body := Body}) ->
#{<<"position">> := Position} = Body,
case emqx_authz:move(RuleId, Position) of
{ok, _} -> {204};
{error, not_found_rule} ->
{404, #{code => <<"NOT_FOUND">>,
messgae => <<"rule ", RuleId/binary, " not found">>}};
{error, Reason} ->
{400, #{code => <<"BAD_REQUEST">>,
messgae => atom_to_binary(Reason)}}
end.

View File

@ -19,137 +19,491 @@
-export([definitions/0]).
definitions() ->
RetruenedRules = #{
RetruenedSources = #{
allOf => [ #{type => object,
properties => #{
annotations => #{
type => object,
required => [id],
required => [status],
properties => #{
id => #{
type => string
},
principal => minirest:ref(<<"principal">>)
status => #{
type => string,
example => <<"healthy">>
}
}
}
}
, minirest:ref(<<"rules">>)
}
, minirest:ref(<<"sources">>)
]
},
Rules = #{
oneOf => [ minirest:ref(<<"simple_rule">>)
% , minirest:ref(<<"connector_redis">>)
Sources = #{
oneOf => [ minirest:ref(<<"http">>)
, minirest:ref(<<"mongo_single">>)
, minirest:ref(<<"mongo_rs">>)
, minirest:ref(<<"mongo_sharded">>)
, minirest:ref(<<"mysql">>)
, minirest:ref(<<"pgsql">>)
, minirest:ref(<<"redis_single">>)
, minirest:ref(<<"redis_sentinel">>)
, minirest:ref(<<"redis_cluster">>)
, minirest:ref(<<"file">>)
]
},
% ConnectorRedis = #{
% type => object,
% required => [principal, type, enable, config, cmd]
% properties => #{
% principal => minirest:ref(<<"principal">>),
% type => #{
% type => string,
% enum => [<<"redis">>],
% example => <<"redis">>
% },
% enable => #{
% type => boolean,
% example => true
% }
% config => #{
% type =>
% }
% }
% }
SimpleRule = #{
SSL = #{
type => object,
required => [principal, permission, action, topics],
required => [enable],
properties => #{
action => #{
type => string,
enum => [<<"publish">>, <<"subscribe">>, <<"all">>],
example => <<"publish">>
enable => #{type => boolean, example => true},
cacertfile => #{type => string},
keyfile => #{type => string},
certfile => #{type => string},
verify => #{type => boolean, example => false}
}
},
permission => #{
HTTP = #{
type => object,
required => [ type
, enable
, method
, headers
, request_timeout
, connect_timeout
, max_retries
, retry_interval
, pool_type
, pool_size
, enable_pipelining
, ssl
],
properties => #{
type => #{
type => string,
enum => [<<"allow">>, <<"deny">>],
example => <<"allow">>
enum => [<<"http">>],
example => <<"http">>
},
topics => #{
enable => #{
type => boolean,
example => true
},
url => #{
type => string,
example => <<"https://emqx.com">>
},
method => #{
type => string,
enum => [<<"get">>, <<"post">>, <<"put">>],
example => <<"get">>
},
headers => #{type => object},
body => #{type => object},
connect_timeout => #{type => integer},
max_retries => #{type => integer},
retry_interval => #{type => integer},
pool_type => #{
type => string,
enum => [<<"random">>, <<"hash">>],
example => <<"random">>
},
pool_size => #{type => integer},
enable_pipelining => #{type => boolean},
ssl => minirest:ref(<<"ssl">>)
}
},
MongoSingle= #{
type => object,
required => [ type
, enable
, collection
, find
, mongo_type
, server
, pool_size
, username
, password
, auth_source
, database
, topology
, ssl
],
properties => #{
type => #{
type => string,
enum => [<<"mongo">>],
example => <<"mongo">>
},
enable => #{
type => boolean,
example => true
},
collection => #{type => string},
find => #{type => object},
mongo_type => #{type => string,
enum => [<<"single">>],
example => <<"single">>},
server => #{type => string, example => <<"127.0.0.1:27017">>},
pool_size => #{type => integer},
username => #{type => string},
password => #{type => string},
auth_source => #{type => string},
database => #{type => string},
topology => #{type => object,
properties => #{
pool_size => #{type => integer},
max_overflow => #{type => integer},
overflow_ttl => #{type => integer},
overflow_check_period => #{type => integer},
local_threshold_ms => #{type => integer},
connect_timeout_ms => #{type => integer},
socket_timeout_ms => #{type => integer},
server_selection_timeout_ms => #{type => integer},
wait_queue_timeout_ms => #{type => integer},
heartbeat_frequency_ms => #{type => integer},
min_heartbeat_frequency_ms => #{type => integer}
}
},
ssl => minirest:ref(<<"ssl">>)
}
},
MongoRs= #{
type => object,
required => [ type
, enable
, collection
, find
, mongo_type
, servers
, replica_set_name
, pool_size
, username
, password
, auth_source
, database
, topology
, ssl
],
properties => #{
type => #{
type => string,
enum => [<<"mongo">>],
example => <<"mongo">>
},
enable => #{
type => boolean,
example => true
},
collection => #{type => string},
find => #{type => object},
mongo_type => #{type => string,
enum => [<<"rs">>],
example => <<"rs">>},
servers => #{type => array,
items => #{type => string,example => <<"127.0.0.1:27017">>}},
replica_set_name => #{type => string},
pool_size => #{type => integer},
username => #{type => string},
password => #{type => string},
auth_source => #{type => string},
database => #{type => string},
topology => #{type => object,
properties => #{
pool_size => #{type => integer},
max_overflow => #{type => integer},
overflow_ttl => #{type => integer},
overflow_check_period => #{type => integer},
local_threshold_ms => #{type => integer},
connect_timeout_ms => #{type => integer},
socket_timeout_ms => #{type => integer},
server_selection_timeout_ms => #{type => integer},
wait_queue_timeout_ms => #{type => integer},
heartbeat_frequency_ms => #{type => integer},
min_heartbeat_frequency_ms => #{type => integer}
}
},
ssl => minirest:ref(<<"ssl">>)
}
},
MongoSharded = #{
type => object,
required => [ type
, enable
, collection
, find
, mongo_type
, servers
, pool_size
, username
, password
, auth_source
, database
, topology
, ssl
],
properties => #{
type => #{
type => string,
enum => [<<"mongo">>],
example => <<"mongo">>
},
enable => #{
type => boolean,
example => true
},
collection => #{type => string},
find => #{type => object},
mongo_type => #{type => string,
enum => [<<"sharded">>],
example => <<"sharded">>},
servers => #{type => array,
items => #{type => string,example => <<"127.0.0.1:27017">>}},
pool_size => #{type => integer},
username => #{type => string},
password => #{type => string},
auth_source => #{type => string},
database => #{type => string},
topology => #{type => object,
properties => #{
pool_size => #{type => integer},
max_overflow => #{type => integer},
overflow_ttl => #{type => integer},
overflow_check_period => #{type => integer},
local_threshold_ms => #{type => integer},
connect_timeout_ms => #{type => integer},
socket_timeout_ms => #{type => integer},
server_selection_timeout_ms => #{type => integer},
wait_queue_timeout_ms => #{type => integer},
heartbeat_frequency_ms => #{type => integer},
min_heartbeat_frequency_ms => #{type => integer}
}
},
ssl => minirest:ref(<<"ssl">>)
}
},
Mysql = #{
type => object,
required => [ type
, enable
, sql
, server
, database
, pool_size
, username
, password
, auto_reconnect
, ssl
],
properties => #{
type => #{
type => string,
enum => [<<"mysql">>],
example => <<"mysql">>
},
enable => #{
type => boolean,
example => true
},
sql => #{type => string},
server => #{type => string,
example => <<"127.0.0.1:3306">>
},
database => #{type => string},
pool_size => #{type => integer},
username => #{type => string},
password => #{type => string},
auto_reconnect => #{type => boolean,
example => true
},
ssl => minirest:ref(<<"ssl">>)
}
},
Pgsql = #{
type => object,
required => [ type
, enable
, sql
, server
, database
, pool_size
, username
, password
, auto_reconnect
, ssl
],
properties => #{
type => #{
type => string,
enum => [<<"pgsql">>],
example => <<"pgsql">>
},
enable => #{
type => boolean,
example => true
},
sql => #{type => string},
server => #{type => string,
example => <<"127.0.0.1:5432">>
},
database => #{type => string},
pool_size => #{type => integer},
username => #{type => string},
password => #{type => string},
auto_reconnect => #{type => boolean,
example => true
},
ssl => minirest:ref(<<"ssl">>)
}
},
RedisSingle = #{
type => object,
required => [ type
, enable
, cmd
, server
, redis_type
, pool_size
, auto_reconnect
, ssl
],
properties => #{
type => #{
type => string,
enum => [<<"redis">>],
example => <<"redis">>
},
enable => #{
type => boolean,
example => true
},
cmd => #{
type => string,
example => <<"HGETALL mqtt_authz">>
},
server => #{type => string, example => <<"127.0.0.1:3306">>},
redis_type => #{type => string,
enum => [<<"single">>],
example => <<"single">>},
pool_size => #{type => integer},
auto_reconnect => #{type => boolean, example => true},
password => #{type => string},
database => #{type => integer},
ssl => minirest:ref(<<"ssl">>)
}
},
RedisSentinel= #{
type => object,
required => [ type
, enable
, cmd
, servers
, redis_type
, sentinel
, pool_size
, auto_reconnect
, ssl
],
properties => #{
type => #{
type => string,
enum => [<<"redis">>],
example => <<"redis">>
},
enable => #{
type => boolean,
example => true
},
cmd => #{
type => string,
example => <<"HGETALL mqtt_authz">>
},
servers => #{type => array,
items => #{type => string,example => <<"127.0.0.1:3306">>}},
redis_type => #{type => string,
enum => [<<"sentinel">>],
example => <<"sentinel">>},
sentinel => #{type => string},
pool_size => #{type => integer},
auto_reconnect => #{type => boolean, example => true},
password => #{type => string},
database => #{type => integer},
ssl => minirest:ref(<<"ssl">>)
}
},
RedisCluster= #{
type => object,
required => [ type
, enable
, cmd
, servers
, redis_type
, pool_size
, auto_reconnect
, ssl],
properties => #{
type => #{
type => string,
enum => [<<"redis">>],
example => <<"redis">>
},
enable => #{
type => boolean,
example => true
},
cmd => #{
type => string,
example => <<"HGETALL mqtt_authz">>
},
servers => #{type => array,
items => #{type => string, example => <<"127.0.0.1:3306">>}},
redis_type => #{type => string,
enum => [<<"cluster">>],
example => <<"cluster">>},
pool_size => #{type => integer},
auto_reconnect => #{type => boolean, example => true},
password => #{type => string},
database => #{type => integer},
ssl => minirest:ref(<<"ssl">>)
}
},
File = #{
type => object,
required => [type, enable, rules],
properties => #{
type => #{
type => string,
enum => [<<"redis">>],
example => <<"redis">>
},
enable => #{
type => boolean,
example => true
},
rules => #{
type => array,
items => #{
oneOf => [ #{type => string, example => <<"#">>}
, #{type => object,
required => [eq],
properties => #{
eq => #{type => string}
},
example => #{eq => <<"#">>}
}
]
}
},
principal => minirest:ref(<<"principal">>)
}
},
Principal = #{
oneOf => [ minirest:ref(<<"principal_username">>)
, minirest:ref(<<"principal_clientid">>)
, minirest:ref(<<"principal_ipaddress">>)
, #{type => string, enum=>[<<"all">>], example => <<"all">>}
, #{type => object,
required => ['and'],
properties => #{'and' => #{type => array,
items => #{oneOf => [ minirest:ref(<<"principal_username">>)
, minirest:ref(<<"principal_clientid">>)
, minirest:ref(<<"principal_ipaddress">>)
]}}},
example => #{'and' => [#{username => <<"emqx">>}, #{clientid => <<"emqx">>}]}
}
, #{type => object,
required => ['or'],
properties => #{'and' => #{type => array,
items => #{oneOf => [ minirest:ref(<<"principal_username">>)
, minirest:ref(<<"principal_clientid">>)
, minirest:ref(<<"principal_ipaddress">>)
]}}},
example => #{'or' => [#{username => <<"emqx">>}, #{clientid => <<"emqx">>}]}
}
]
},
PrincipalUsername = #{type => object,
required => [username],
properties => #{username => #{type => string}},
example => #{username => <<"emqx">>}
},
PrincipalClientid = #{type => object,
required => [clientid],
properties => #{clientid => #{type => string}},
example => #{clientid => <<"emqx">>}
},
PrincipalIpaddress = #{type => object,
required => [ipaddress],
properties => #{ipaddress => #{type => string}},
example => #{ipaddress => <<"127.0.0.1">>}
},
ErrorDef = #{
type => object,
properties => #{
code => #{
type => string,
example => <<"BAD_REQUEST">>
example => <<"{allow,{username,\"^dashboard?\"},subscribe,[\"$SYS/#\"]}.">>
}
},
message => #{
type => string
path => #{
type => string,
example => <<"/path/to/authorizaiton_rules.conf">>
}
}
},
[ #{<<"returned_rules">> => RetruenedRules}
, #{<<"rules">> => Rules}
, #{<<"simple_rule">> => SimpleRule}
, #{<<"principal">> => Principal}
, #{<<"principal_username">> => PrincipalUsername}
, #{<<"principal_clientid">> => PrincipalClientid}
, #{<<"principal_ipaddress">> => PrincipalIpaddress}
, #{<<"error">> => ErrorDef}
[ #{<<"returned_sources">> => RetruenedSources}
, #{<<"sources">> => Sources}
, #{<<"ssl">> => SSL}
, #{<<"http">> => HTTP}
, #{<<"mongo_single">> => MongoSingle}
, #{<<"mongo_rs">> => MongoRs}
, #{<<"mongo_sharded">> => MongoSharded}
, #{<<"mysql">> => Mysql}
, #{<<"pgsql">> => Pgsql}
, #{<<"redis_single">> => RedisSingle}
, #{<<"redis_sentinel">> => RedisSentinel}
, #{<<"redis_cluster">> => RedisCluster}
, #{<<"file">> => File}
].

View File

@ -0,0 +1,61 @@
%%--------------------------------------------------------------------
%% 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_settings).
-behavior(minirest_api).
-export([ api_spec/0
, settings/2
]).
api_spec() ->
{[settings_api()], []}.
authorization_settings() ->
maps:remove(<<"sources">>, emqx:get_raw_config([authorization], #{})).
conf_schema() ->
emqx_mgmt_api_configs:gen_schema(authorization_settings()).
settings_api() ->
Metadata = #{
get => #{
description => "Get authorization settings",
responses => #{<<"200">> => emqx_mgmt_util:schema(conf_schema())}
},
put => #{
description => "Update authorization settings",
requestBody => emqx_mgmt_util:schema(conf_schema()),
responses => #{
<<"200">> => emqx_mgmt_util:schema(conf_schema()),
<<"400">> => emqx_mgmt_util:bad_request()
}
}
},
{"/authorization/settings", Metadata, settings}.
settings(get, _Params) ->
{200, authorization_settings()};
settings(put, #{body := #{<<"no_match">> := NoMatch,
<<"deny_action">> := DenyAction,
<<"cache">> := Cache}}) ->
{ok, _} = emqx:update_config([authorization, no_match], NoMatch),
{ok, _} = emqx:update_config([authorization, deny_action], DenyAction),
{ok, _} = emqx:update_config([authorization, cache], Cache),
ok = emqx_authz_cache:drain_cache(),
{200, authorization_settings()}.

View File

@ -0,0 +1,493 @@
%%--------------------------------------------------------------------
%% 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_sources).
-behavior(minirest_api).
-include("emqx_authz.hrl").
-include_lib("emqx/include/logger.hrl").
-define(EXAMPLE_REDIS,
#{type=> redis,
enable => true,
server => <<"127.0.0.1:3306">>,
redis_type => single,
pool_size => 1,
auto_reconnect => true,
cmd => <<"HGETALL mqtt_authz">>}).
-define(EXAMPLE_FILE,
#{type=> file,
enable => true,
rules => [<<"{allow,{username,\"^dashboard?\"},subscribe,[\"$SYS/#\"]}.">>,
<<"{allow,{ipaddr,\"127.0.0.1\"},all,[\"$SYS/#\",\"#\"]}.">>
]}).
-define(EXAMPLE_RETURNED_REDIS,
maps:put(annotations, #{status => healthy}, ?EXAMPLE_REDIS)
).
-define(EXAMPLE_RETURNED_FILE,
maps:put(annotations, #{status => healthy}, ?EXAMPLE_FILE)
).
-define(EXAMPLE_RETURNED,
#{sources => [ ?EXAMPLE_RETURNED_REDIS
, ?EXAMPLE_RETURNED_FILE
]
}).
-export([ api_spec/0
, sources/2
, source/2
, move_source/2
]).
api_spec() ->
{[ sources_api()
, source_api()
, move_source_api()
], definitions()}.
definitions() -> emqx_authz_api_schema:definitions().
sources_api() ->
Metadata = #{
get => #{
description => "List authorization sources",
responses => #{
<<"200">> => #{
description => <<"OK">>,
content => #{
'application/json' => #{
schema => #{
type => object,
required => [sources],
properties => #{sources => #{
type => array,
items => minirest:ref(<<"returned_sources">>)
}
}
},
examples => #{
sources => #{
summary => <<"Sources">>,
value => jsx:encode(?EXAMPLE_RETURNED)
}
}
}
}
}
}
},
post => #{
description => "Add new source",
requestBody => #{
content => #{
'application/json' => #{
schema => minirest:ref(<<"sources">>),
examples => #{
redis => #{
summary => <<"Redis">>,
value => jsx:encode(?EXAMPLE_REDIS)
},
file => #{
summary => <<"File">>,
value => jsx:encode(?EXAMPLE_FILE)
}
}
}
}
},
responses => #{
<<"204">> => #{description => <<"Created">>},
<<"400">> => emqx_mgmt_util:bad_request()
}
},
put => #{
description => "Update all sources",
requestBody => #{
content => #{
'application/json' => #{
schema => #{
type => array,
items => minirest:ref(<<"returned_sources">>)
},
examples => #{
redis => #{
summary => <<"Redis">>,
value => jsx:encode(?EXAMPLE_REDIS)
},
file => #{
summary => <<"File">>,
value => jsx:encode(?EXAMPLE_FILE)
}
}
}
}
},
responses => #{
<<"204">> => #{description => <<"Created">>},
<<"400">> => emqx_mgmt_util:bad_request()
}
}
},
{"/authorization/sources", Metadata, sources}.
source_api() ->
Metadata = #{
get => #{
description => "List authorization sources",
parameters => [
#{
name => type,
in => path,
schema => #{
type => string
},
required => true
}
],
responses => #{
<<"200">> => #{
description => <<"OK">>,
content => #{
'application/json' => #{
schema => minirest:ref(<<"returned_sources">>),
examples => #{
redis => #{
summary => <<"Redis">>,
value => jsx:encode(?EXAMPLE_RETURNED_REDIS)
},
file => #{
summary => <<"File">>,
value => jsx:encode(?EXAMPLE_RETURNED_FILE)
}
}
}
}
},
<<"404">> => emqx_mgmt_util:bad_request(<<"Not Found">>)
}
},
put => #{
description => "Update source",
parameters => [
#{
name => type,
in => path,
schema => #{
type => string
},
required => true
}
],
requestBody => #{
content => #{
'application/json' => #{
schema => minirest:ref(<<"sources">>),
examples => #{
redis => #{
summary => <<"Redis">>,
value => jsx:encode(?EXAMPLE_REDIS)
},
file => #{
summary => <<"File">>,
value => jsx:encode(?EXAMPLE_FILE)
}
}
}
}
},
responses => #{
<<"204">> => #{description => <<"No Content">>},
<<"404">> => emqx_mgmt_util:bad_request(<<"Not Found">>),
<<"400">> => emqx_mgmt_util:bad_request()
}
},
delete => #{
description => "Delete source",
parameters => [
#{
name => type,
in => path,
schema => #{
type => string
},
required => true
}
],
responses => #{
<<"204">> => #{description => <<"No Content">>},
<<"400">> => emqx_mgmt_util:bad_request()
}
}
},
{"/authorization/sources/:type", Metadata, source}.
move_source_api() ->
Metadata = #{
post => #{
description => "Change the order of sources",
parameters => [
#{
name => type,
in => path,
schema => #{
type => string
},
required => true
}
],
requestBody => #{
content => #{
'application/json' => #{
schema => #{
type => object,
required => [position],
properties => #{
position => #{
oneOf => [
#{type => string,
enum => [<<"top">>, <<"bottom">>]
},
#{type => object,
required => ['after'],
properties => #{
'after' => #{
type => string
}
}
},
#{type => object,
required => ['before'],
properties => #{
'before' => #{
type => string
}
}
}
]
}
}
}
}
}
},
responses => #{
<<"204">> => #{
description => <<"No Content">>
},
<<"404">> => emqx_mgmt_util:bad_request(<<"Not Found">>),
<<"400">> => emqx_mgmt_util:bad_request()
}
}
},
{"/authorization/sources/:type/move", Metadata, move_source}.
sources(get, _) ->
Sources = lists:foldl(fun (#{type := file, enable := Enable, path := Path}, AccIn) ->
{ok, Rules} = file:consult(Path),
lists:append(AccIn, [#{type => file,
enable => Enable,
rules => [ iolist_to_binary(io_lib:format("~p.", [R])) || R <- Rules],
annotations => #{status => healthy}
}]);
(#{enable := false} = Source, AccIn) ->
lists:append(AccIn, [Source#{annotations => #{status => unhealthy}}]);
(#{type := _Type, annotations := #{id := Id}} = Source, AccIn) ->
NSource0 = case maps:get(server, Source, undefined) of
undefined -> Source;
Server ->
Source#{server => emqx_connector_schema_lib:ip_port_to_string(Server)}
end,
NSource1 = case maps:get(servers, Source, undefined) of
undefined -> NSource0;
Servers ->
NSource0#{servers => [emqx_connector_schema_lib:ip_port_to_string(Server) || Server <- Servers]}
end,
NSource2 = case emqx_resource:health_check(Id) of
ok ->
NSource1#{annotations => #{status => healthy}};
_ ->
NSource1#{annotations => #{status => unhealthy}}
end,
lists:append(AccIn, [read_cert(NSource2)]);
(Source, AccIn) ->
lists:append(AccIn, [Source#{annotations => #{status => healthy}}])
end, [], emqx_authz:lookup()),
{200, #{sources => Sources}};
sources(post, #{body := #{<<"type">> := <<"file">>, <<"rules">> := Rules, <<"enable">> := Enable}}) when is_list(Rules) ->
{ok, Filename} = write_file(filename:join([emqx:get_config([node, data_dir]), "acl.conf"]),
erlang:list_to_bitstring([<<Rule/binary, "\n">> || Rule <- Rules])
),
case emqx_authz:update(head, [#{type => file, enable => Enable, path => Filename}]) of
{ok, _} -> {204};
{error, Reason} ->
{400, #{code => <<"BAD_REQUEST">>,
messgae => atom_to_binary(Reason)}}
end;
sources(post, #{body := Body}) when is_map(Body) ->
case emqx_authz:update(head, [write_cert(Body)]) of
{ok, _} -> {204};
{error, Reason} ->
{400, #{code => <<"BAD_REQUEST">>,
messgae => atom_to_binary(Reason)}}
end;
sources(put, #{body := Body}) when is_list(Body) ->
NBody = [ begin
case Source of
#{<<"type">> := <<"file">>, <<"rules">> := Rules, <<"enable">> := Enable} ->
{ok, Filename} = write_file(filename:join([emqx:get_config([node, data_dir]), "acl.conf"]),
erlang:list_to_bitstring([<<Rule/binary, "\n">> || Rule <- Rules])
),
#{type => file, enable => Enable, path => Filename};
_ -> write_cert(Source)
end
end || Source <- Body],
case emqx_authz:update(replace, NBody) of
{ok, _} -> {204};
{error, Reason} ->
{400, #{code => <<"BAD_REQUEST">>,
messgae => atom_to_binary(Reason)}}
end.
source(get, #{bindings := #{type := Type}}) ->
case emqx_authz:lookup(Type) of
{error, Reason} -> {404, #{messgae => atom_to_binary(Reason)}};
#{type := file, enable := Enable, path := Path}->
{ok, Rules} = file:consult(Path),
{200, #{type => file,
enable => Enable,
rules => [ iolist_to_binary(io_lib:format("~p.", [R])) || R <- Rules],
annotations => #{status => healthy}
}
};
#{enable := false} = Source -> {200, Source#{annotations => #{status => unhealthy}}};
#{annotations := #{id := Id}} = Source ->
NSource0 = case maps:get(server, Source, undefined) of
undefined -> Source;
Server ->
Source#{server => emqx_connector_schema_lib:ip_port_to_string(Server)}
end,
NSource1 = case maps:get(servers, Source, undefined) of
undefined -> NSource0;
Servers ->
NSource0#{servers => [emqx_connector_schema_lib:ip_port_to_string(Server) || Server <- Servers]}
end,
NSource2 = case emqx_resource:health_check(Id) of
ok ->
NSource1#{annotations => #{status => healthy}};
_ ->
NSource1#{annotations => #{status => unhealthy}}
end,
{200, read_cert(NSource2)}
end;
source(put, #{bindings := #{type := <<"file">>}, body := #{<<"type">> := <<"file">>, <<"rules">> := Rules, <<"enable">> := Enable}}) ->
{ok, Filename} = write_file(maps:get(path, emqx_authz:lookup(file), ""),
erlang:list_to_bitstring([<<Rule/binary, "\n">> || Rule <- Rules])
),
case emqx_authz:update({replace_once, file}, #{type => file, enable => Enable, path => Filename}) of
{ok, _} -> {204};
{error, Reason} ->
{400, #{code => <<"BAD_REQUEST">>,
messgae => atom_to_binary(Reason)}}
end;
source(put, #{bindings := #{type := Type}, body := Body}) when is_map(Body) ->
case emqx_authz:update({replace_once, Type}, write_cert(Body)) of
{ok, _} -> {204};
{error, not_found_source} ->
{404, #{code => <<"NOT_FOUND">>,
messgae => <<"source ", Type/binary, " not found">>}};
{error, Reason} ->
{400, #{code => <<"BAD_REQUEST">>,
messgae => atom_to_binary(Reason)}}
end;
source(delete, #{bindings := #{type := Type}}) ->
case emqx_authz:update({delete_once, Type}, #{}) of
{ok, _} -> {204};
{error, Reason} ->
{400, #{code => <<"BAD_REQUEST">>,
messgae => atom_to_binary(Reason)}}
end.
move_source(post, #{bindings := #{type := Type}, body := #{<<"position">> := Position}}) ->
case emqx_authz:move(Type, Position) of
{ok, _} -> {204};
{error, not_found_source} ->
{404, #{code => <<"NOT_FOUND">>,
messgae => <<"source ", Type/binary, " not found">>}};
{error, Reason} ->
{400, #{code => <<"BAD_REQUEST">>,
messgae => atom_to_binary(Reason)}}
end.
read_cert(#{ssl := #{enable := true} = SSL} = Source) ->
CaCert = case file:read_file(maps:get(cacertfile, SSL, "")) of
{ok, CaCert0} -> CaCert0;
_ -> ""
end,
Cert = case file:read_file(maps:get(certfile, SSL, "")) of
{ok, Cert0} -> Cert0;
_ -> ""
end,
Key = case file:read_file(maps:get(keyfile, SSL, "")) of
{ok, Key0} -> Key0;
_ -> ""
end,
Source#{ssl => SSL#{cacertfile => CaCert,
certfile => Cert,
keyfile => Key
}
};
read_cert(Source) -> Source.
write_cert(#{<<"ssl">> := #{<<"enable">> := true} = SSL} = Source) ->
CertPath = filename:join([emqx:get_config([node, data_dir]), "certs"]),
CaCert = case maps:is_key(<<"cacertfile">>, SSL) of
true ->
{ok, CaCertFile} = write_file(filename:join([CertPath, "cacert-" ++ emqx_rule_id:gen() ++".pem"]),
maps:get(<<"cacertfile">>, SSL)),
CaCertFile;
false -> ""
end,
Cert = case maps:is_key(<<"certfile">>, SSL) of
true ->
{ok, CertFile} = write_file(filename:join([CertPath, "cert-" ++ emqx_rule_id:gen() ++".pem"]),
maps:get(<<"certfile">>, SSL)),
CertFile;
false -> ""
end,
Key = case maps:is_key(<<"keyfile">>, SSL) of
true ->
{ok, KeyFile} = write_file(filename:join([CertPath, "key-" ++ emqx_rule_id:gen() ++".pem"]),
maps:get(<<"keyfile">>, SSL)),
KeyFile;
false -> ""
end,
Source#{<<"ssl">> => SSL#{<<"cacertfile">> => CaCert,
<<"certfile">> => Cert,
<<"keyfile">> => Key
}
};
write_cert(Source) -> Source.
write_file(Filename, Bytes) ->
ok = filelib:ensure_dir(Filename),
case file:write_file(Filename, Bytes) of
ok -> {ok, iolist_to_binary(Filename)};
{error, Reason} ->
?LOG(error, "Write File ~p Error: ~p", [Filename, Reason]),
error(Reason)
end.

View File

@ -35,12 +35,12 @@ description() ->
authorize(Client, PubSub, Topic,
#{type := http,
config := #{url := #{path := Path} = Url,
url := #{path := Path} = Url,
headers := Headers,
method := Method,
request_timeout := RequestTimeout} = Config,
request_timeout := RequestTimeout,
annotations := #{id := ResourceID}
}) ->
} = Source) ->
Request = case Method of
get ->
Query = maps:get(query, Url, ""),
@ -49,7 +49,7 @@ authorize(Client, PubSub, Topic,
_ ->
Body0 = serialize_body(
maps:get('Accept', Headers, <<"application/json">>),
maps:get(body, Config, #{})
maps:get(body, Source, #{})
),
Body1 = replvar(Body0, PubSub, Topic, Client),
Path1 = replvar(Path, PubSub, Topic, Client),

View File

@ -13,14 +13,32 @@
-type permission() :: allow | deny.
-type url() :: emqx_http_lib:uri_map().
-export([ roots/0
-export([ namespace/0
, roots/0
, fields/1
]).
roots() -> ["authorization"].
namespace() -> authz.
%% @doc authorization schema is not exported
%% but directly used by emqx_schema
roots() -> [].
fields("authorization") ->
[ {sources, sources()}
[ {sources, #{type => union_array(
[ hoconsc:ref(?MODULE, file)
, hoconsc:ref(?MODULE, http_get)
, hoconsc:ref(?MODULE, http_post)
, hoconsc:ref(?MODULE, mongo_single)
, hoconsc:ref(?MODULE, mongo_rs)
, hoconsc:ref(?MODULE, mongo_sharded)
, hoconsc:ref(?MODULE, mysql)
, hoconsc:ref(?MODULE, pgsql)
, hoconsc:ref(?MODULE, redis_single)
, hoconsc:ref(?MODULE, redis_sentinel)
, hoconsc:ref(?MODULE, redis_cluster)
])}
}
];
fields(file) ->
[ {type, #{type => file}}
@ -34,17 +52,12 @@ fields(file) ->
end
}}
];
fields(http) ->
fields(http_get) ->
[ {type, #{type => http}}
, {enable, #{type => boolean(),
default => true}}
, {config, #{type => hoconsc:union([ hoconsc:ref(?MODULE, http_get)
, hoconsc:ref(?MODULE, http_post)
])}
}
];
fields(http_get) ->
[ {url, #{type => url()}}
, {url, #{type => url()}}
, {method, #{type => get, default => get }}
, {headers, #{type => map(),
default => #{ <<"accept">> => <<"application/json">>
, <<"cache-control">> => <<"no-cache">>
@ -64,11 +77,15 @@ fields(http_get) ->
end
}
}
, {method, #{type => get, default => get }}
, {request_timeout, #{type => timeout(), default => 30000 }}
] ++ proplists:delete(base_url, emqx_connector_http:fields(config));
fields(http_post) ->
[ {url, #{type => url()}}
[ {type, #{type => http}}
, {enable, #{type => boolean(),
default => true}}
, {url, #{type => url()}}
, {method, #{type => hoconsc:enum([post, put]),
default => get}}
, {headers, #{type => map(),
default => #{ <<"accept">> => <<"application/json">>
, <<"cache-control">> => <<"no-cache">>
@ -90,54 +107,42 @@ fields(http_post) ->
end
}
}
, {method, #{type => hoconsc:enum([post, put]),
default => get}}
, {request_timeout, #{type => timeout(), default => 30000 }}
, {body, #{type => map(),
nullable => true
}
}
] ++ proplists:delete(base_url, emqx_connector_http:fields(config));
fields(mongo) ->
connector_fields(mongo) ++
fields(mongo_single) ->
connector_fields(mongo, single) ++
[ {collection, #{type => atom()}}
, {find, #{type => map()}}
];
fields(mongo_rs) ->
connector_fields(mongo, rs) ++
[ {collection, #{type => atom()}}
, {find, #{type => map()}}
];
fields(mongo_sharded) ->
connector_fields(mongo, sharded) ++
[ {collection, #{type => atom()}}
, {find, #{type => map()}}
];
fields(redis) ->
connector_fields(redis) ++
[ {cmd, query()} ];
fields(mysql) ->
connector_fields(mysql) ++
[ {sql, query()} ];
fields(pgsql) ->
connector_fields(pgsql) ++
[ {sql, query()} ];
fields(username) ->
[{username, #{type => binary()}}];
fields(clientid) ->
[{clientid, #{type => binary()}}];
fields(ipaddress) ->
[{ipaddress, #{type => string()}}];
fields(andlist) ->
[{'and', #{type => union_array(
[ hoconsc:ref(?MODULE, username)
, hoconsc:ref(?MODULE, clientid)
, hoconsc:ref(?MODULE, ipaddress)
])
}
}
];
fields(orlist) ->
[{'or', #{type => union_array(
[ hoconsc:ref(?MODULE, username)
, hoconsc:ref(?MODULE, clientid)
, hoconsc:ref(?MODULE, ipaddress)
])
}
}
];
fields(eq_topic) ->
[{eq, #{type => binary()}}].
fields(redis_single) ->
connector_fields(redis, single) ++
[ {cmd, query()} ];
fields(redis_sentinel) ->
connector_fields(redis, sentinel) ++
[ {cmd, query()} ];
fields(redis_cluster) ->
connector_fields(redis, cluster) ++
[ {cmd, query()} ].
%%--------------------------------------------------------------------
%% Internal functions
@ -146,17 +151,6 @@ fields(eq_topic) ->
union_array(Item) when is_list(Item) ->
hoconsc:array(hoconsc:union(Item)).
sources() ->
#{type => union_array(
[ hoconsc:ref(?MODULE, file)
, hoconsc:ref(?MODULE, http)
, hoconsc:ref(?MODULE, mysql)
, hoconsc:ref(?MODULE, pgsql)
, hoconsc:ref(?MODULE, redis)
, hoconsc:ref(?MODULE, mongo)
])
}.
query() ->
#{type => binary(),
validator => fun(S) ->
@ -168,6 +162,8 @@ query() ->
}.
connector_fields(DB) ->
connector_fields(DB, config).
connector_fields(DB, Fields) ->
Mod0 = io_lib:format("~s_~s",[emqx_connector, DB]),
Mod = try
list_to_existing_atom(Mod0)
@ -180,4 +176,4 @@ connector_fields(DB) ->
[ {type, #{type => DB}}
, {enable, #{type => boolean(),
default => true}}
] ++ Mod:roots().
] ++ Mod:fields(Fields).

View File

@ -61,120 +61,141 @@ init_per_testcase(_, Config) ->
Config.
-define(SOURCE1, #{<<"type">> => <<"http">>,
<<"config">> => #{
<<"enable">> => true,
<<"url">> => <<"https://fake.com:443/">>,
<<"headers">> => #{},
<<"method">> => <<"get">>,
<<"request_timeout">> => 5000}
<<"request_timeout">> => 5000
}).
-define(SOURCE2, #{<<"type">> => <<"mongo">>,
<<"config">> => #{
<<"enable">> => true,
<<"mongo_type">> => <<"single">>,
<<"server">> => <<"127.0.0.1:27017">>,
<<"pool_size">> => 1,
<<"database">> => <<"mqtt">>,
<<"ssl">> => #{<<"enable">> => false}},
<<"ssl">> => #{<<"enable">> => false},
<<"collection">> => <<"fake">>,
<<"find">> => #{<<"a">> => <<"b">>}
}).
-define(SOURCE3, #{<<"type">> => <<"mysql">>,
<<"config">> => #{
<<"enable">> => true,
<<"server">> => <<"127.0.0.1:27017">>,
<<"pool_size">> => 1,
<<"database">> => <<"mqtt">>,
<<"username">> => <<"xx">>,
<<"password">> => <<"ee">>,
<<"auto_reconnect">> => true,
<<"ssl">> => #{<<"enable">> => false}},
<<"ssl">> => #{<<"enable">> => false},
<<"sql">> => <<"abcb">>
}).
-define(SOURCE4, #{<<"type">> => <<"pgsql">>,
<<"config">> => #{
<<"enable">> => true,
<<"server">> => <<"127.0.0.1:27017">>,
<<"pool_size">> => 1,
<<"database">> => <<"mqtt">>,
<<"username">> => <<"xx">>,
<<"password">> => <<"ee">>,
<<"auto_reconnect">> => true,
<<"ssl">> => #{<<"enable">> => false}},
<<"ssl">> => #{<<"enable">> => false},
<<"sql">> => <<"abcb">>
}).
-define(SOURCE5, #{<<"type">> => <<"redis">>,
<<"config">> => #{
<<"enable">> => true,
<<"server">> => <<"127.0.0.1:27017">>,
<<"pool_size">> => 1,
<<"database">> => 0,
<<"password">> => <<"ee">>,
<<"auto_reconnect">> => true,
<<"ssl">> => #{<<"enable">> => false}},
<<"ssl">> => #{<<"enable">> => false},
<<"cmd">> => <<"HGETALL mqtt_authz:%u">>
}).
-define(SOURCE6, #{<<"type">> => <<"file">>,
<<"enable">> => true,
<<"path">> => emqx_ct_helpers:deps_path(emqx_authz, "etc/acl.conf")
}).
%%------------------------------------------------------------------------------
%% Testcases
%%------------------------------------------------------------------------------
t_update_source(_) ->
{ok, _} = emqx_authz:update(replace, [?SOURCE2]),
{ok, _} = emqx_authz:update(replace, [?SOURCE3]),
{ok, _} = emqx_authz:update(head, [?SOURCE2]),
{ok, _} = emqx_authz:update(head, [?SOURCE1]),
{ok, _} = emqx_authz:update(tail, [?SOURCE3]),
{ok, _} = emqx_authz:update(tail, [?SOURCE4]),
{ok, _} = emqx_authz:update(tail, [?SOURCE5]),
{ok, _} = emqx_authz:update(tail, [?SOURCE6]),
?assertMatch([#{type := http}, #{type := mongo}, #{type := mysql}], emqx:get_config([authorization, sources], [])),
?assertMatch([ #{type := http, enable := true}
, #{type := mongo, enable := true}
, #{type := mysql, enable := true}
, #{type := pgsql, enable := true}
, #{type := redis, enable := true}
, #{type := file, enable := true}
], emqx:get_config([authorization, sources], [])),
[#{annotations := #{id := Id1}, type := http},
#{annotations := #{id := Id2}, type := mongo},
#{annotations := #{id := Id3}, type := mysql}
] = emqx_authz:lookup(),
{ok, _} = emqx_authz:update({replace_once, http}, ?SOURCE1#{<<"enable">> := false}),
{ok, _} = emqx_authz:update({replace_once, mongo}, ?SOURCE2#{<<"enable">> := false}),
{ok, _} = emqx_authz:update({replace_once, mysql}, ?SOURCE3#{<<"enable">> := false}),
{ok, _} = emqx_authz:update({replace_once, pgsql}, ?SOURCE4#{<<"enable">> := false}),
{ok, _} = emqx_authz:update({replace_once, redis}, ?SOURCE5#{<<"enable">> := false}),
{ok, _} = emqx_authz:update({replace_once, file}, ?SOURCE6#{<<"enable">> := false}),
{ok, _} = emqx_authz:update({replace_once, Id1}, ?SOURCE5),
{ok, _} = emqx_authz:update({replace_once, Id3}, ?SOURCE4),
?assertMatch([#{type := redis}, #{type := mongo}, #{type := pgsql}], emqx:get_config([authorization, sources], [])),
[#{annotations := #{id := Id1}, type := redis},
#{annotations := #{id := Id2}, type := mongo},
#{annotations := #{id := Id3}, type := pgsql}
] = emqx_authz:lookup(),
?assertMatch([ #{type := http, enable := false}
, #{type := mongo, enable := false}
, #{type := mysql, enable := false}
, #{type := pgsql, enable := false}
, #{type := redis, enable := false}
, #{type := file, enable := false}
], emqx:get_config([authorization, sources], [])),
{ok, _} = emqx_authz:update(replace, []).
t_move_source(_) ->
{ok, _} = emqx_authz:update(replace, [?SOURCE1, ?SOURCE2, ?SOURCE3, ?SOURCE4, ?SOURCE5]),
[#{annotations := #{id := Id1}},
#{annotations := #{id := Id2}},
#{annotations := #{id := Id3}},
#{annotations := #{id := Id4}},
#{annotations := #{id := Id5}}
] = emqx_authz:lookup(),
{ok, _} = emqx_authz:move(Id4, <<"top">>),
?assertMatch([#{annotations := #{id := Id4}},
#{annotations := #{id := Id1}},
#{annotations := #{id := Id2}},
#{annotations := #{id := Id3}},
#{annotations := #{id := Id5}}
{ok, _} = emqx_authz:update(replace, [?SOURCE1, ?SOURCE2, ?SOURCE3, ?SOURCE4, ?SOURCE5, ?SOURCE6]),
?assertMatch([ #{type := http}
, #{type := mongo}
, #{type := mysql}
, #{type := pgsql}
, #{type := redis}
, #{type := file}
], emqx_authz:lookup()),
{ok, _} = emqx_authz:move(Id1, <<"bottom">>),
?assertMatch([#{annotations := #{id := Id4}},
#{annotations := #{id := Id2}},
#{annotations := #{id := Id3}},
#{annotations := #{id := Id5}},
#{annotations := #{id := Id1}}
{ok, _} = emqx_authz:move(pgsql, <<"top">>),
?assertMatch([ #{type := pgsql}
, #{type := http}
, #{type := mongo}
, #{type := mysql}
, #{type := redis}
, #{type := file}
], emqx_authz:lookup()),
{ok, _} = emqx_authz:move(Id3, #{<<"before">> => Id4}),
?assertMatch([#{annotations := #{id := Id3}},
#{annotations := #{id := Id4}},
#{annotations := #{id := Id2}},
#{annotations := #{id := Id5}},
#{annotations := #{id := Id1}}
{ok, _} = emqx_authz:move(http, <<"bottom">>),
?assertMatch([ #{type := pgsql}
, #{type := mongo}
, #{type := mysql}
, #{type := redis}
, #{type := file}
, #{type := http}
], emqx_authz:lookup()),
{ok, _} = emqx_authz:move(Id2, #{<<"after">> => Id1}),
?assertMatch([#{annotations := #{id := Id3}},
#{annotations := #{id := Id4}},
#{annotations := #{id := Id5}},
#{annotations := #{id := Id1}},
#{annotations := #{id := Id2}}
{ok, _} = emqx_authz:move(mysql, #{<<"before">> => pgsql}),
?assertMatch([ #{type := mysql}
, #{type := pgsql}
, #{type := mongo}
, #{type := redis}
, #{type := file}
, #{type := http}
], emqx_authz:lookup()),
{ok, _} = emqx_authz:move(mongo, #{<<"after">> => http}),
?assertMatch([ #{type := mysql}
, #{type := pgsql}
, #{type := redis}
, #{type := file}
, #{type := http}
, #{type := mongo}
], emqx_authz:lookup()),
ok.

View File

@ -1,266 +0,0 @@
%%--------------------------------------------------------------------
%% 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").
-define(CONF_DEFAULT, <<"authorization: {sources: []}">>).
-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
, auth_header/2
]).
-define(HOST, "http://127.0.0.1:18083/").
-define(API_VERSION, "v5").
-define(BASE_PATH, "api").
-define(SOURCE1, #{<<"type">> => <<"http">>,
<<"config">> => #{
<<"url">> => <<"https://fake.com:443/">>,
<<"headers">> => #{},
<<"method">> => <<"get">>,
<<"request_timeout">> => 5000}
}).
-define(SOURCE2, #{<<"type">> => <<"mongo">>,
<<"config">> => #{
<<"mongo_type">> => <<"single">>,
<<"server">> => <<"127.0.0.1:27017">>,
<<"pool_size">> => 1,
<<"database">> => <<"mqtt">>,
<<"ssl">> => #{<<"enable">> => false}},
<<"collection">> => <<"fake">>,
<<"find">> => #{<<"a">> => <<"b">>}
}).
-define(SOURCE3, #{<<"type">> => <<"mysql">>,
<<"config">> => #{
<<"server">> => <<"127.0.0.1:27017">>,
<<"pool_size">> => 1,
<<"database">> => <<"mqtt">>,
<<"username">> => <<"xx">>,
<<"password">> => <<"ee">>,
<<"auto_reconnect">> => true,
<<"ssl">> => #{<<"enable">> => false}},
<<"sql">> => <<"abcb">>
}).
-define(SOURCE4, #{<<"type">> => <<"pgsql">>,
<<"config">> => #{
<<"server">> => <<"127.0.0.1:27017">>,
<<"pool_size">> => 1,
<<"database">> => <<"mqtt">>,
<<"username">> => <<"xx">>,
<<"password">> => <<"ee">>,
<<"auto_reconnect">> => true,
<<"ssl">> => #{<<"enable">> => false}},
<<"sql">> => <<"abcb">>
}).
-define(SOURCE5, #{<<"type">> => <<"redis">>,
<<"config">> => #{
<<"server">> => <<"127.0.0.1:27017">>,
<<"pool_size">> => 1,
<<"database">> => 0,
<<"password">> => <<"ee">>,
<<"auto_reconnect">> => true,
<<"ssl">> => #{<<"enable">> => false}},
<<"cmd">> => <<"HGETALL mqtt_authz:%u">>
}).
all() ->
emqx_ct:all(?MODULE).
groups() ->
[].
init_per_suite(Config) ->
meck:new(emqx_schema, [non_strict, passthrough, no_history, no_link]),
meck:expect(emqx_schema, fields, fun("authorization") ->
meck:passthrough(["authorization"]) ++
emqx_authz_schema:fields("authorization");
(F) -> meck:passthrough([F])
end),
meck:new(emqx_resource, [non_strict, passthrough, no_history, no_link]),
meck:expect(emqx_resource, create, fun(_, _, _) -> {ok, meck_data} end),
meck:expect(emqx_resource, update, fun(_, _, _, _) -> {ok, meck_data} end),
meck:expect(emqx_resource, health_check, fun(_) -> ok end),
meck:expect(emqx_resource, remove, fun(_) -> ok end ),
ok = emqx_config:init_load(emqx_authz_schema, ?CONF_DEFAULT),
ok = emqx_ct_helpers:start_apps([emqx_authz, emqx_dashboard], fun set_special_configs/1),
{ok, _} = emqx:update_config([authorization, cache, enable], false),
{ok, _} = emqx:update_config([authorization, no_match], deny),
Config.
end_per_suite(_Config) ->
{ok, _} = emqx_authz:update(replace, []),
emqx_ct_helpers:stop_apps([emqx_resource, emqx_authz, emqx_dashboard]),
meck:unload(emqx_resource),
meck:unload(emqx_schema),
ok.
set_special_configs(emqx_dashboard) ->
Config = #{
default_username => <<"admin">>,
default_password => <<"public">>,
listeners => [#{
protocol => http,
port => 18083
}]
},
emqx_config:put([emqx_dashboard], Config),
ok;
set_special_configs(emqx_authz) ->
emqx_config:put([authorization], #{rules => []}),
ok;
set_special_configs(_App) ->
ok.
%%------------------------------------------------------------------------------
%% Testcases
%%------------------------------------------------------------------------------
t_api(_) ->
{ok, 200, Result1} = request(get, uri(["authorization"]), []),
?assertEqual([], get_rules(Result1)),
lists:foreach(fun(_) ->
{ok, 204, _} = request(post, uri(["authorization"]), ?SOURCE1)
end, lists:seq(1, 20)),
{ok, 200, Result2} = request(get, uri(["authorization"]), []),
?assertEqual(20, length(get_rules(Result2))),
lists:foreach(fun(Page) ->
Query = "?page=" ++ integer_to_list(Page) ++ "&&limit=10",
Url = uri(["authorization" ++ Query]),
{ok, 200, Result} = request(get, Url, []),
?assertEqual(10, length(get_rules(Result)))
end, lists:seq(1, 2)),
{ok, 204, _} = request(put, uri(["authorization"]), [?SOURCE1, ?SOURCE2, ?SOURCE3, ?SOURCE4]),
{ok, 200, Result3} = request(get, uri(["authorization"]), []),
Rules = get_rules(Result3),
?assertEqual(4, length(Rules)),
?assertMatch([ #{<<"type">> := <<"http">>}
, #{<<"type">> := <<"mongo">>}
, #{<<"type">> := <<"mysql">>}
, #{<<"type">> := <<"pgsql">>}
], Rules),
#{<<"annotations">> := #{<<"id">> := Id}} = lists:nth(2, Rules),
{ok, 204, _} = request(put, uri(["authorization", binary_to_list(Id)]), ?SOURCE5),
{ok, 200, Result4} = request(get, uri(["authorization", binary_to_list(Id)]), []),
?assertMatch(#{<<"type">> := <<"redis">>}, jsx:decode(Result4)),
lists:foreach(fun(#{<<"annotations">> := #{<<"id">> := Id0}}) ->
{ok, 204, _} = request(delete, uri(["authorization", binary_to_list(Id0)]), [])
end, Rules),
{ok, 200, Result5} = request(get, uri(["authorization"]), []),
?assertEqual([], get_rules(Result5)),
ok.
t_move_rule(_) ->
{ok, _} = emqx_authz:update(replace, [?SOURCE1, ?SOURCE2, ?SOURCE3, ?SOURCE4, ?SOURCE5]),
[#{annotations := #{id := Id1}},
#{annotations := #{id := Id2}},
#{annotations := #{id := Id3}},
#{annotations := #{id := Id4}},
#{annotations := #{id := Id5}}
] = emqx_authz:lookup(),
{ok, 204, _} = request(post, uri(["authorization", Id4, "move"]),
#{<<"position">> => <<"top">>}),
?assertMatch([#{annotations := #{id := Id4}},
#{annotations := #{id := Id1}},
#{annotations := #{id := Id2}},
#{annotations := #{id := Id3}},
#{annotations := #{id := Id5}}
], emqx_authz:lookup()),
{ok, 204, _} = request(post, uri(["authorization", Id1, "move"]),
#{<<"position">> => <<"bottom">>}),
?assertMatch([#{annotations := #{id := Id4}},
#{annotations := #{id := Id2}},
#{annotations := #{id := Id3}},
#{annotations := #{id := Id5}},
#{annotations := #{id := Id1}}
], emqx_authz:lookup()),
{ok, 204, _} = request(post, uri(["authorization", Id3, "move"]),
#{<<"position">> => #{<<"before">> => Id4}}),
?assertMatch([#{annotations := #{id := Id3}},
#{annotations := #{id := Id4}},
#{annotations := #{id := Id2}},
#{annotations := #{id := Id5}},
#{annotations := #{id := Id1}}
], emqx_authz:lookup()),
{ok, 204, _} = request(post, uri(["authorization", Id2, "move"]),
#{<<"position">> => #{<<"after">> => Id1}}),
?assertMatch([#{annotations := #{id := Id3}},
#{annotations := #{id := Id4}},
#{annotations := #{id := Id5}},
#{annotations := #{id := Id1}},
#{annotations := #{id := Id2}}
], emqx_authz:lookup()),
ok.
%%--------------------------------------------------------------------
%% HTTP Request
%%--------------------------------------------------------------------
request(Method, Url, Body) ->
Request = case Body of
[] -> {Url, [auth_header_()]};
_ -> {Url, [auth_header_()], "application/json", jsx:encode(Body)}
end,
ct:pal("Method: ~p, Request: ~p", [Method, Request]),
case httpc:request(Method, Request, [], [{body_format, binary}]) of
{error, socket_closed_remotely} ->
{error, socket_closed_remotely};
{ok, {{"HTTP/1.1", Code, _}, _Headers, Return} } ->
{ok, Code, Return};
{ok, {Reason, _, _}} ->
{error, Reason}
end.
uri() -> uri([]).
uri(Parts) when is_list(Parts) ->
NParts = [E || E <- Parts],
?HOST ++ filename:join([?BASE_PATH, ?API_VERSION | NParts]).
get_rules(Result) ->
maps:get(<<"rules">>, jsx:decode(Result), []).
auth_header_() ->
Username = <<"admin">>,
Password = <<"public">>,
{ok, Token} = emqx_dashboard_admin:sign_token(Username, Password),
{"Authorization", "Bearer " ++ binary_to_list(Token)}.

View File

@ -0,0 +1,135 @@
%%--------------------------------------------------------------------
%% 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_settings_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").
-define(CONF_DEFAULT, <<"authorization: {sources: []}">>).
-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
, auth_header/2
]).
-define(HOST, "http://127.0.0.1:18083/").
-define(API_VERSION, "v5").
-define(BASE_PATH, "api").
all() ->
emqx_ct:all(?MODULE).
groups() ->
[].
init_per_suite(Config) ->
ok = emqx_ct_helpers:start_apps([emqx_authz, emqx_dashboard], fun set_special_configs/1),
{ok, _} = emqx:update_config([authorization, cache, enable], false),
{ok, _} = emqx:update_config([authorization, no_match], deny),
Config.
end_per_suite(_Config) ->
emqx_ct_helpers:stop_apps([emqx_resource, emqx_authz, emqx_dashboard]),
ok.
set_special_configs(emqx_dashboard) ->
Config = #{
default_username => <<"admin">>,
default_password => <<"public">>,
listeners => [#{
protocol => http,
port => 18083
}]
},
emqx_config:put([emqx_dashboard], Config),
ok;
set_special_configs(_App) ->
ok.
%%------------------------------------------------------------------------------
%% Testcases
%%------------------------------------------------------------------------------
t_api(_) ->
Settings1 = #{<<"no_match">> => <<"deny">>,
<<"deny_action">> => <<"disconnect">>,
<<"cache">> => #{
<<"enable">> => false,
<<"max_size">> => 32,
<<"ttl">> => 60000
}
},
{ok, 200, Result1} = request(put, uri(["authorization", "settings"]), Settings1),
{ok, 200, Result1} = request(get, uri(["authorization", "settings"]), []),
?assertEqual(Settings1, jsx:decode(Result1)),
Settings2 = #{<<"no_match">> => <<"allow">>,
<<"deny_action">> => <<"ignore">>,
<<"cache">> => #{
<<"enable">> => true,
<<"max_size">> => 32,
<<"ttl">> => 60000
}
},
{ok, 200, Result2} = request(put, uri(["authorization", "settings"]), Settings2),
{ok, 200, Result2} = request(get, uri(["authorization", "settings"]), []),
?assertEqual(Settings2, jsx:decode(Result2)),
ok.
%%--------------------------------------------------------------------
%% HTTP Request
%%--------------------------------------------------------------------
request(Method, Url, Body) ->
Request = case Body of
[] -> {Url, [auth_header_()]};
_ -> {Url, [auth_header_()], "application/json", jsx:encode(Body)}
end,
ct:pal("Method: ~p, Request: ~p", [Method, Request]),
case httpc:request(Method, Request, [], [{body_format, binary}]) of
{error, socket_closed_remotely} ->
{error, socket_closed_remotely};
{ok, {{"HTTP/1.1", Code, _}, _Headers, Return} } ->
{ok, Code, Return};
{ok, {Reason, _, _}} ->
{error, Reason}
end.
uri() -> uri([]).
uri(Parts) when is_list(Parts) ->
NParts = [E || E <- Parts],
?HOST ++ filename:join([?BASE_PATH, ?API_VERSION | NParts]).
get_sources(Result) ->
maps:get(<<"sources">>, jsx:decode(Result), []).
auth_header_() ->
Username = <<"admin">>,
Password = <<"public">>,
{ok, Token} = emqx_dashboard_admin:sign_token(Username, Password),
{"Authorization", "Bearer " ++ binary_to_list(Token)}.

View File

@ -0,0 +1,305 @@
%%--------------------------------------------------------------------
%% 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_sources_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").
-define(CONF_DEFAULT, <<"authorization: {sources: []}">>).
-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
, auth_header/2
]).
-define(HOST, "http://127.0.0.1:18083/").
-define(API_VERSION, "v5").
-define(BASE_PATH, "api").
-define(SOURCE1, #{<<"type">> => <<"http">>,
<<"enable">> => true,
<<"url">> => <<"https://fake.com:443/">>,
<<"headers">> => #{},
<<"method">> => <<"get">>,
<<"request_timeout">> => 5000
}).
-define(SOURCE2, #{<<"type">> => <<"mongo">>,
<<"enable">> => true,
<<"mongo_type">> => <<"sharded">>,
<<"servers">> => [<<"127.0.0.1:27017">>,
<<"192.168.0.1:27017">>
],
<<"pool_size">> => 1,
<<"database">> => <<"mqtt">>,
<<"ssl">> => #{<<"enable">> => false},
<<"collection">> => <<"fake">>,
<<"find">> => #{<<"a">> => <<"b">>}
}).
-define(SOURCE3, #{<<"type">> => <<"mysql">>,
<<"enable">> => true,
<<"server">> => <<"127.0.0.1:3306">>,
<<"pool_size">> => 1,
<<"database">> => <<"mqtt">>,
<<"username">> => <<"xx">>,
<<"password">> => <<"ee">>,
<<"auto_reconnect">> => true,
<<"ssl">> => #{<<"enable">> => false},
<<"sql">> => <<"abcb">>
}).
-define(SOURCE4, #{<<"type">> => <<"pgsql">>,
<<"enable">> => true,
<<"server">> => <<"127.0.0.1:5432">>,
<<"pool_size">> => 1,
<<"database">> => <<"mqtt">>,
<<"username">> => <<"xx">>,
<<"password">> => <<"ee">>,
<<"auto_reconnect">> => true,
<<"ssl">> => #{<<"enable">> => false},
<<"sql">> => <<"abcb">>
}).
-define(SOURCE5, #{<<"type">> => <<"redis">>,
<<"enable">> => true,
<<"servers">> => [<<"127.0.0.1:6379">>,
<<"127.0.0.1:6380">>
],
<<"pool_size">> => 1,
<<"database">> => 0,
<<"password">> => <<"ee">>,
<<"auto_reconnect">> => true,
<<"ssl">> => #{<<"enable">> => false},
<<"cmd">> => <<"HGETALL mqtt_authz:%u">>
}).
-define(SOURCE6, #{<<"type">> => <<"file">>,
<<"enable">> => true,
<<"rules">> =>
[<<"{allow,{username,\"^dashboard?\"},subscribe,[\"$SYS/#\"]}.">>,
<<"{allow,{ipaddr,\"127.0.0.1\"},all,[\"$SYS/#\",\"#\"]}.">>
]
}).
all() ->
emqx_ct:all(?MODULE).
groups() ->
[].
init_per_suite(Config) ->
meck:new(emqx_schema, [non_strict, passthrough, no_history, no_link]),
meck:expect(emqx_schema, fields, fun("authorization") ->
meck:passthrough(["authorization"]) ++
emqx_authz_schema:fields("authorization");
(F) -> meck:passthrough([F])
end),
meck:new(emqx_resource, [non_strict, passthrough, no_history, no_link]),
meck:expect(emqx_resource, create, fun(_, _, _) -> {ok, meck_data} end),
meck:expect(emqx_resource, update, fun(_, _, _, _) -> {ok, meck_data} end),
meck:expect(emqx_resource, health_check, fun(_) -> ok end),
meck:expect(emqx_resource, remove, fun(_) -> ok end ),
ok = emqx_config:init_load(emqx_authz_schema, ?CONF_DEFAULT),
ok = emqx_ct_helpers:start_apps([emqx_authz, emqx_dashboard], fun set_special_configs/1),
{ok, _} = emqx:update_config([authorization, cache, enable], false),
{ok, _} = emqx:update_config([authorization, no_match], deny),
Config.
end_per_suite(_Config) ->
{ok, _} = emqx_authz:update(replace, []),
emqx_ct_helpers:stop_apps([emqx_resource, emqx_authz, emqx_dashboard]),
meck:unload(emqx_resource),
meck:unload(emqx_schema),
ok.
set_special_configs(emqx_dashboard) ->
Config = #{
default_username => <<"admin">>,
default_password => <<"public">>,
listeners => [#{
protocol => http,
port => 18083
}]
},
emqx_config:put([emqx_dashboard], Config),
ok;
set_special_configs(emqx_authz) ->
emqx_config:put([authorization], #{sources => []}),
ok;
set_special_configs(_App) ->
ok.
init_per_testcase(t_api, Config) ->
meck:new(emqx_rule_id, [non_strict, passthrough, no_history, no_link]),
meck:expect(emqx_rule_id, gen, fun() -> "fake" end),
meck:new(emqx, [non_strict, passthrough, no_history, no_link]),
meck:expect(emqx, get_config, fun([node, data_dir]) ->
% emqx_ct_helpers:deps_path(emqx_authz, "test");
{data_dir, Data} = lists:keyfind(data_dir, 1, Config),
Data;
(C) -> meck:passthrough([C])
end),
Config;
init_per_testcase(_, Config) -> Config.
end_per_testcase(t_api, _Config) ->
meck:unload(emqx_rule_id),
meck:unload(emqx),
ok;
end_per_testcase(_, _Config) -> ok.
%%------------------------------------------------------------------------------
%% Testcases
%%------------------------------------------------------------------------------
t_api(_) ->
{ok, 200, Result1} = request(get, uri(["authorization", "sources"]), []),
?assertEqual([], get_sources(Result1)),
{ok, 204, _} = request(put, uri(["authorization", "sources"]), [?SOURCE2, ?SOURCE3, ?SOURCE4, ?SOURCE5, ?SOURCE6]),
{ok, 204, _} = request(post, uri(["authorization", "sources"]), ?SOURCE1),
{ok, 200, Result2} = request(get, uri(["authorization", "sources"]), []),
Sources = get_sources(Result2),
?assertMatch([ #{<<"type">> := <<"http">>}
, #{<<"type">> := <<"mongo">>}
, #{<<"type">> := <<"mysql">>}
, #{<<"type">> := <<"pgsql">>}
, #{<<"type">> := <<"redis">>}
, #{<<"type">> := <<"file">>}
], Sources),
?assert(filelib:is_file(filename:join([emqx:get_config([node, data_dir]), "acl.conf"]))),
{ok, 204, _} = request(put, uri(["authorization", "sources", "http"]), ?SOURCE1#{<<"enable">> := false}),
{ok, 200, Result3} = request(get, uri(["authorization", "sources", "http"]), []),
?assertMatch(#{<<"type">> := <<"http">>, <<"enable">> := false}, jsx:decode(Result3)),
{ok, 204, _} = request(put, uri(["authorization", "sources", "mongo"]),
?SOURCE2#{<<"ssl">> := #{
<<"enable">> => true,
<<"cacertfile">> => <<"fake cacert file">>,
<<"certfile">> => <<"fake cert file">>,
<<"keyfile">> => <<"fake key file">>,
<<"verify">> => false
}}),
{ok, 200, Result4} = request(get, uri(["authorization", "sources", "mongo"]), []),
?assertMatch(#{<<"type">> := <<"mongo">>,
<<"ssl">> := #{<<"enable">> := true,
<<"cacertfile">> := <<"fake cacert file">>,
<<"certfile">> := <<"fake cert file">>,
<<"keyfile">> := <<"fake key file">>,
<<"verify">> := false
}
}, jsx:decode(Result4)),
?assert(filelib:is_file(filename:join([emqx:get_config([node, data_dir]), "certs", "cacert-fake.pem"]))),
?assert(filelib:is_file(filename:join([emqx:get_config([node, data_dir]), "certs", "cert-fake.pem"]))),
?assert(filelib:is_file(filename:join([emqx:get_config([node, data_dir]), "certs", "key-fake.pem"]))),
lists:foreach(fun(#{<<"type">> := Type}) ->
{ok, 204, _} = request(delete, uri(["authorization", "sources", binary_to_list(Type)]), [])
end, Sources),
{ok, 200, Result5} = request(get, uri(["authorization", "sources"]), []),
?assertEqual([], get_sources(Result5)),
ok.
t_move_source(_) ->
{ok, _} = emqx_authz:update(replace, [?SOURCE1, ?SOURCE2, ?SOURCE3, ?SOURCE4, ?SOURCE5]),
?assertMatch([ #{type := http}
, #{type := mongo}
, #{type := mysql}
, #{type := pgsql}
, #{type := redis}
], emqx_authz:lookup()),
{ok, 204, _} = request(post, uri(["authorization", "sources", "pgsql", "move"]),
#{<<"position">> => <<"top">>}),
?assertMatch([ #{type := pgsql}
, #{type := http}
, #{type := mongo}
, #{type := mysql}
, #{type := redis}
], emqx_authz:lookup()),
{ok, 204, _} = request(post, uri(["authorization", "sources", "http", "move"]),
#{<<"position">> => <<"bottom">>}),
?assertMatch([ #{type := pgsql}
, #{type := mongo}
, #{type := mysql}
, #{type := redis}
, #{type := http}
], emqx_authz:lookup()),
{ok, 204, _} = request(post, uri(["authorization", "sources", "mysql", "move"]),
#{<<"position">> => #{<<"before">> => <<"pgsql">>}}),
?assertMatch([ #{type := mysql}
, #{type := pgsql}
, #{type := mongo}
, #{type := redis}
, #{type := http}
], emqx_authz:lookup()),
{ok, 204, _} = request(post, uri(["authorization", "sources", "mongo", "move"]),
#{<<"position">> => #{<<"after">> => <<"http">>}}),
?assertMatch([ #{type := mysql}
, #{type := pgsql}
, #{type := redis}
, #{type := http}
, #{type := mongo}
], emqx_authz:lookup()),
ok.
%%--------------------------------------------------------------------
%% HTTP Request
%%--------------------------------------------------------------------
request(Method, Url, Body) ->
Request = case Body of
[] -> {Url, [auth_header_()]};
_ -> {Url, [auth_header_()], "application/json", jsx:encode(Body)}
end,
ct:pal("Method: ~p, Request: ~p", [Method, Request]),
case httpc:request(Method, Request, [], [{body_format, binary}]) of
{error, socket_closed_remotely} ->
{error, socket_closed_remotely};
{ok, {{"HTTP/1.1", Code, _}, _Headers, Return} } ->
{ok, Code, Return};
{ok, {Reason, _, _}} ->
{error, Reason}
end.
uri() -> uri([]).
uri(Parts) when is_list(Parts) ->
NParts = [E || E <- Parts],
?HOST ++ filename:join([?BASE_PATH, ?API_VERSION | NParts]).
get_sources(Result) ->
maps:get(<<"sources">>, jsx:decode(Result), []).
auth_header_() ->
Username = <<"admin">>,
Password = <<"public">>,
{ok, Token} = emqx_dashboard_admin:sign_token(Username, Password),
{"Authorization", "Bearer " ++ binary_to_list(Token)}.

View File

@ -47,12 +47,11 @@ init_per_suite(Config) ->
{ok, _} = emqx:update_config([authorization, cache, enable], false),
{ok, _} = emqx:update_config([authorization, no_match], deny),
Rules = [#{<<"type">> => <<"http">>,
<<"config">> => #{
<<"url">> => <<"https://fake.com:443/">>,
<<"headers">> => #{},
<<"method">> => <<"get">>,
<<"request_timeout">> => 5000
}}
}
],
{ok, _} = emqx_authz:update(replace, Rules),
Config.

View File

@ -47,12 +47,11 @@ init_per_suite(Config) ->
{ok, _} = emqx:update_config([authorization, cache, enable], false),
{ok, _} = emqx:update_config([authorization, no_match], deny),
Rules = [#{<<"type">> => <<"mongo">>,
<<"config">> => #{
<<"mongo_type">> => <<"single">>,
<<"server">> => <<"127.0.0.1:27017">>,
<<"pool_size">> => 1,
<<"database">> => <<"mqtt">>,
<<"ssl">> => #{<<"enable">> => false}},
<<"ssl">> => #{<<"enable">> => false},
<<"collection">> => <<"fake">>,
<<"find">> => #{<<"a">> => <<"b">>}
}],

View File

@ -48,14 +48,13 @@ init_per_suite(Config) ->
{ok, _} = emqx:update_config([authorization, cache, enable], false),
{ok, _} = emqx:update_config([authorization, no_match], deny),
Rules = [#{<<"type">> => <<"mysql">>,
<<"config">> => #{
<<"server">> => <<"127.0.0.1:27017">>,
<<"pool_size">> => 1,
<<"database">> => <<"mqtt">>,
<<"username">> => <<"xx">>,
<<"password">> => <<"ee">>,
<<"auto_reconnect">> => true,
<<"ssl">> => #{<<"enable">> => false}},
<<"ssl">> => #{<<"enable">> => false},
<<"sql">> => <<"abcb">>
}],
{ok, _} = emqx_authz:update(replace, Rules),

View File

@ -48,14 +48,13 @@ init_per_suite(Config) ->
{ok, _} = emqx:update_config([authorization, cache, enable], false),
{ok, _} = emqx:update_config([authorization, no_match], deny),
Rules = [#{<<"type">> => <<"pgsql">>,
<<"config">> => #{
<<"server">> => <<"127.0.0.1:27017">>,
<<"pool_size">> => 1,
<<"database">> => <<"mqtt">>,
<<"username">> => <<"xx">>,
<<"password">> => <<"ee">>,
<<"auto_reconnect">> => true,
<<"ssl">> => #{<<"enable">> => false}},
<<"ssl">> => #{<<"enable">> => false},
<<"sql">> => <<"abcb">>
}],
{ok, _} = emqx_authz:update(replace, Rules),

View File

@ -47,13 +47,12 @@ init_per_suite(Config) ->
{ok, _} = emqx:update_config([authorization, cache, enable], false),
{ok, _} = emqx:update_config([authorization, no_match], deny),
Rules = [#{<<"type">> => <<"redis">>,
<<"config">> => #{
<<"server">> => <<"127.0.0.1:27017">>,
<<"pool_size">> => 1,
<<"database">> => 0,
<<"password">> => <<"ee">>,
<<"auto_reconnect">> => true,
<<"ssl">> => #{<<"enable">> => false}},
<<"ssl">> => #{<<"enable">> => false},
<<"cmd">> => <<"HGETALL mqtt_authz:%u">>
}],
{ok, _} = emqx_authz:update(replace, Rules),

View File

@ -4,10 +4,12 @@ auto_subscribe {
# {
# topic = "/c/${clientid}",
# qos = 0
# },
# rh = 0
# rap = 0
# nl = 0
# }
# {
# topic = "/u/${username}",
# qos = 1
# },
# {
# topic = "/h/${host}",
@ -15,15 +17,12 @@ auto_subscribe {
# },
# {
# topic = "/p/${port}",
# qos = 0
# },
# {
# topic = "/topic/abc",
# qos = 0
# },
# {
# topic = "/client/${clientid}/username/${username}/host/${host}/port/${port}",
# qos = 0
# }
]
}

View File

@ -23,6 +23,7 @@
-export([auto_subscribe/2]).
-define(EXCEED_LIMIT, 'EXCEED_LIMIT').
-define(BAD_REQUEST, 'BAD_REQUEST').
api_spec() ->
{[auto_subscribe_api()], []}.
@ -30,7 +31,7 @@ api_spec() ->
schema() ->
emqx_mgmt_util:schema(
emqx_mgmt_api_configs:gen_schema(
emqx:get_raw_config([auto_subscribe, topics]))).
emqx:get_raw_config([auto_subscribe, topics])), <<"">>).
auto_subscribe_api() ->
Metadata = #{
@ -43,6 +44,8 @@ auto_subscribe_api() ->
'requestBody' => schema(),
responses => #{
<<"200">> => schema(),
<<"400">> => emqx_mgmt_util:error_schema(
<<"Request body required">>, [?BAD_REQUEST]),
<<"409">> => emqx_mgmt_util:error_schema(
<<"Auto Subscribe topics max limit">>, [?EXCEED_LIMIT])}}
},
@ -53,6 +56,8 @@ auto_subscribe_api() ->
auto_subscribe(get, _) ->
{200, emqx_auto_subscribe:list()};
auto_subscribe(put, #{body := #{}}) ->
{400, #{code => ?BAD_REQUEST, message => <<"Request body required">>}};
auto_subscribe(put, #{body := Params}) ->
case emqx_auto_subscribe:update(Params) of
{error, quota_exceeded} ->

View File

@ -23,16 +23,17 @@
generate(Topics) when is_list(Topics) ->
[generate(Topic) || Topic <- Topics];
generate(#{qos := Qos, topic := Topic}) when is_binary(Topic) ->
#{qos => Qos, placeholder => generate(Topic, [])}.
generate(T0 = #{topic := Topic}) ->
T = maps:without([topic], T0),
T#{placeholder => generate(Topic, [])}.
-spec(to_topic_table(list(), map(), map()) -> list()).
to_topic_table(PlaceHolders, ClientInfo, ConnInfo) ->
to_topic_table(PHs, ClientInfo, ConnInfo) ->
[begin
Topic0 = to_topic(PlaceHolder, ClientInfo, ConnInfo, []),
{Topic, Opts} = emqx_topic:parse(Topic0),
{Topic, Opts#{qos => Qos}}
end || #{qos := Qos, placeholder := PlaceHolder} <- PlaceHolders].
{Topic, Opts#{qos => Qos, rh => RH, rap => RAP, nl => NL}}
end || #{qos := Qos, rh := RH, rap := RAP, nl := NL, placeholder := PlaceHolder} <- PHs].
%%--------------------------------------------------------------------
%% internal

View File

@ -19,16 +19,30 @@
-include_lib("typerefl/include/types.hrl").
-export([ roots/0
-export([ namespace/0
, roots/0
, fields/1]).
namespace() -> "auto_subscribe".
roots() ->
["auto_subscribe"].
fields("auto_subscribe") ->
[ {topics, hoconsc:array(hoconsc:ref(?MODULE, "topic"))}];
[ {topics, hoconsc:array(hoconsc:ref(?MODULE, "topic"))}
];
fields("topic") ->
[ {topic, emqx_schema:t(binary())}
, {qos, emqx_schema:t(integer(), undefined, 0)}
[ {topic, sc(binary(), #{})}
, {qos, sc(typerefl:union([0, 1, 2]), #{default => 0})}
, {rh, sc(typerefl:union([0, 1, 2]), #{default => 0})}
, {rap, sc(typerefl:union([0, 1]), #{default => 0})}
, {nl, sc(typerefl:union([0, 1]), #{default => 0})}
].
%%--------------------------------------------------------------------
%% Internal functions
%%--------------------------------------------------------------------
sc(Type, Meta) ->
hoconsc:mk(Type, Meta).

View File

@ -1,4 +1,4 @@
# emqx_data_bridge
# emqx_bridge
EMQ X Data Bridge is an application that managing the resources (see emqx_resource) used by emqx
rule engine.

View File

@ -0,0 +1,49 @@
##--------------------------------------------------------------------
## EMQ X Bridge
##--------------------------------------------------------------------
#bridges.mqtt.my_mqtt_bridge {
# server = "127.0.0.1:1883"
# proto_ver = "v4"
# ## the clientid will be the concatenation of `clientid_prefix` and ids in `in` and `out`.
# clientid_prefix = "bridge_client:"
# username = "username1"
# password = ""
# clean_start = true
# keepalive = 300
# retry_interval = "30s"
# max_inflight = 32
# reconnect_interval = "30s"
# bridge_mode = true
# replayq {
# dir = "{{ platform_data_dir }}/replayq/bridge_mqtt/"
# seg_bytes = "100MB"
# offload = false
# max_total_bytes = "1GB"
# }
# ssl {
# enable = false
# keyfile = "{{ platform_etc_dir }}/certs/client-key.pem"
# certfile = "{{ platform_etc_dir }}/certs/client-cert.pem"
# cacertfile = "{{ platform_etc_dir }}/certs/cacert.pem"
# }
# ## we will create one MQTT connection for each element of the `in`
# in: [{
# id = "pull_msgs_from_aws"
# subscribe_remote_topic = "aws/#"
# subscribe_qos = 1
# local_topic = "from_aws/${topic}"
# payload = "${payload}"
# qos = "${qos}"
# retain = "${retain}"
# }]
# ## we will create one MQTT connection for each element of the `out`
# out: [{
# id = "push_msgs_to_aws"
# subscribe_local_topic = "emqx/#"
# remote_topic = "from_emqx/${topic}"
# payload = "${payload}"
# qos = 1
# retain = false
# }]
#}

View File

@ -3,5 +3,5 @@
{shell, [
% {config, "config/sys.config"},
{apps, [emqx_data_bridge]}
{apps, [emqx_bridge]}
]}.

View File

@ -1,12 +1,13 @@
{application, emqx_data_bridge,
{application, emqx_bridge,
[{description, "An OTP application"},
{vsn, "0.1.0"},
{registered, []},
{mod, {emqx_data_bridge_app, []}},
{mod, {emqx_bridge_app, []}},
{applications,
[kernel,
stdlib,
emqx
emqx,
emqx_connector
]},
{env,[]},
{modules, []},

View File

@ -13,7 +13,7 @@
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_data_bridge).
-module(emqx_bridge).
-export([ load_bridges/0
, resource_type/1
@ -27,15 +27,17 @@
]).
load_bridges() ->
Bridges = emqx:get_config([emqx_data_bridge, bridges], []),
emqx_data_bridge_monitor:ensure_all_started(Bridges).
Bridges = emqx:get_config([bridges], #{}),
emqx_bridge_monitor:ensure_all_started(Bridges).
resource_type(mqtt) -> emqx_connector_mqtt;
resource_type(mysql) -> emqx_connector_mysql;
resource_type(pgsql) -> emqx_connector_pgsql;
resource_type(mongo) -> emqx_connector_mongo;
resource_type(redis) -> emqx_connector_redis;
resource_type(ldap) -> emqx_connector_ldap.
bridge_type(emqx_connector_mqtt) -> mqtt;
bridge_type(emqx_connector_mysql) -> mysql;
bridge_type(emqx_connector_pgsql) -> pgsql;
bridge_type(emqx_connector_mongo) -> mongo;
@ -43,13 +45,14 @@ bridge_type(emqx_connector_redis) -> redis;
bridge_type(emqx_connector_ldap) -> ldap.
name_to_resource_id(BridgeName) ->
<<"bridge:", BridgeName/binary>>.
Name = bin(BridgeName),
<<"bridge:", Name/binary>>.
resource_id_to_name(<<"bridge:", BridgeName/binary>> = _ResourceId) ->
BridgeName.
list_bridges() ->
emqx_resource_api:list_instances(fun emqx_data_bridge:is_bridge/1).
emqx_resource_api:list_instances(fun emqx_bridge:is_bridge/1).
is_bridge(#{id := <<"bridge:", _/binary>>}) ->
true;
@ -57,7 +60,11 @@ is_bridge(_Data) ->
false.
config_key_path() ->
[emqx_data_bridge, bridges].
[emqx_bridge, bridges].
update_config(ConfigReq) ->
emqx:update_config(config_key_path(), ConfigReq).
bin(Bin) when is_binary(Bin) -> Bin;
bin(Str) when is_list(Str) -> list_to_binary(Str);
bin(Atom) when is_atom(Atom) -> atom_to_binary(Atom, utf8).

View File

@ -13,7 +13,7 @@
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_data_bridge_api).
-module(emqx_bridge_api).
-rest_api(#{ name => list_data_bridges
, method => 'GET'
@ -61,10 +61,10 @@
list_bridges(_Binding, _Params) ->
{200, #{code => 0, data => [format_api_reply(Data) ||
Data <- emqx_data_bridge:list_bridges()]}}.
Data <- emqx_bridge:list_bridges()]}}.
get_bridge(#{name := Name}, _Params) ->
case emqx_resource:get_instance(emqx_data_bridge:name_to_resource_id(Name)) of
case emqx_resource:get_instance(emqx_bridge:name_to_resource_id(Name)) of
{ok, Data} ->
{200, #{code => 0, data => format_api_reply(emqx_resource_api:format_data(Data))}};
{error, not_found} ->
@ -75,8 +75,8 @@ create_bridge(#{name := Name}, Params) ->
Config = proplists:get_value(<<"config">>, Params),
BridgeType = proplists:get_value(<<"type">>, Params),
case emqx_resource:check_and_create(
emqx_data_bridge:name_to_resource_id(Name),
emqx_data_bridge:resource_type(atom(BridgeType)), maps:from_list(Config)) of
emqx_bridge:name_to_resource_id(Name),
emqx_bridge:resource_type(atom(BridgeType)), maps:from_list(Config)) of
{ok, already_created} ->
{400, #{code => 102, message => <<"bridge already created: ", Name/binary>>}};
{ok, Data} ->
@ -91,8 +91,8 @@ update_bridge(#{name := Name}, Params) ->
Config = proplists:get_value(<<"config">>, Params),
BridgeType = proplists:get_value(<<"type">>, Params),
case emqx_resource:check_and_update(
emqx_data_bridge:name_to_resource_id(Name),
emqx_data_bridge:resource_type(atom(BridgeType)), maps:from_list(Config), []) of
emqx_bridge:name_to_resource_id(Name),
emqx_bridge:resource_type(atom(BridgeType)), maps:from_list(Config), []) of
{ok, Data} ->
update_config_and_reply(Name, BridgeType, Config, Data);
{error, not_found} ->
@ -104,26 +104,26 @@ update_bridge(#{name := Name}, Params) ->
end.
delete_bridge(#{name := Name}, _Params) ->
case emqx_resource:remove(emqx_data_bridge:name_to_resource_id(Name)) of
case emqx_resource:remove(emqx_bridge:name_to_resource_id(Name)) of
ok -> delete_config_and_reply(Name);
{error, Reason} ->
{500, #{code => 102, message => emqx_resource_api:stringnify(Reason)}}
end.
format_api_reply(#{resource_type := Type, id := Id, config := Conf, status := Status}) ->
#{type => emqx_data_bridge:bridge_type(Type),
name => emqx_data_bridge:resource_id_to_name(Id),
#{type => emqx_bridge:bridge_type(Type),
name => emqx_bridge:resource_id_to_name(Id),
config => Conf, status => Status}.
% format_conf(#{resource_type := Type, id := Id, config := Conf}) ->
% #{type => Type, name => emqx_data_bridge:resource_id_to_name(Id),
% #{type => Type, name => emqx_bridge:resource_id_to_name(Id),
% config => Conf}.
% get_all_configs() ->
% [format_conf(Data) || Data <- emqx_data_bridge:list_bridges()].
% [format_conf(Data) || Data <- emqx_bridge:list_bridges()].
update_config_and_reply(Name, BridgeType, Config, Data) ->
case emqx_data_bridge:update_config({update, ?BRIDGE(Name, BridgeType, Config)}) of
case emqx_bridge:update_config({update, ?BRIDGE(Name, BridgeType, Config)}) of
{ok, _} ->
{200, #{code => 0, data => format_api_reply(
emqx_resource_api:format_data(Data))}};
@ -132,7 +132,7 @@ update_config_and_reply(Name, BridgeType, Config, Data) ->
end.
delete_config_and_reply(Name) ->
case emqx_data_bridge:update_config({delete, Name}) of
case emqx_bridge:update_config({delete, Name}) of
{ok, _} -> {200, #{code => 0, data => #{}}};
{error, Reason} ->
{500, #{code => 102, message => emqx_resource_api:stringnify(Reason)}}

View File

@ -13,7 +13,7 @@
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_data_bridge_app).
-module(emqx_bridge_app).
-behaviour(application).
@ -22,9 +22,9 @@
-export([start/2, stop/1, pre_config_update/2]).
start(_StartType, _StartArgs) ->
{ok, Sup} = emqx_data_bridge_sup:start_link(),
ok = emqx_data_bridge:load_bridges(),
emqx_config_handler:add_handler(emqx_data_bridge:config_key_path(), ?MODULE),
{ok, Sup} = emqx_bridge_sup:start_link(),
ok = emqx_bridge:load_bridges(),
emqx_config_handler:add_handler(emqx_bridge:config_key_path(), ?MODULE),
{ok, Sup}.
stop(_State) ->

View File

@ -15,7 +15,7 @@
%%--------------------------------------------------------------------
%% This process monitors all the data bridges, and try to restart a bridge
%% when one of it stopped.
-module(emqx_data_bridge_monitor).
-module(emqx_bridge_monitor).
-behaviour(gen_server).
@ -65,14 +65,18 @@ code_change(_OldVsn, State, _Extra) ->
%%============================================================================
load_bridges(Configs) ->
lists:foreach(fun load_bridge/1, Configs).
lists:foreach(fun({Type, NamedConf}) ->
lists:foreach(fun({Name, Conf}) ->
load_bridge(Name, Type, Conf)
end, maps:to_list(NamedConf))
end, maps:to_list(Configs)).
%% TODO: move this monitor into emqx_resource
%% emqx_resource:check_and_create_local(ResourceId, ResourceType, Config, #{keep_retry => true}).
load_bridge(#{name := Name, type := Type, config := Config}) ->
load_bridge(Name, Type, Config) ->
case emqx_resource:create_local(
emqx_data_bridge:name_to_resource_id(Name),
emqx_data_bridge:resource_type(Type), Config) of
emqx_bridge:name_to_resource_id(Name),
emqx_bridge:resource_type(Type), Config) of
{ok, already_created} -> ok;
{ok, _} -> ok;
{error, Reason} ->

View File

@ -0,0 +1,17 @@
-module(emqx_bridge_schema).
-export([roots/0, fields/1]).
%%======================================================================================
%% Hocon Schema Definitions
roots() -> ["bridges"].
fields("bridges") ->
[{mqtt, hoconsc:ref(?MODULE, "mqtt")}];
fields("mqtt") ->
[{"$name", hoconsc:ref(?MODULE, "mqtt_bridge")}];
fields("mqtt_bridge") ->
emqx_connector_mqtt:fields("config").

View File

@ -13,7 +13,7 @@
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_data_bridge_sup).
-module(emqx_bridge_sup).
-behaviour(supervisor).
@ -31,11 +31,11 @@ init([]) ->
intensity => 10,
period => 10},
ChildSpecs = [
#{id => emqx_data_bridge_monitor,
start => {emqx_data_bridge_monitor, start_link, []},
#{id => emqx_bridge_monitor,
start => {emqx_bridge_monitor, start_link, []},
restart => permanent,
type => worker,
modules => [emqx_data_bridge_monitor]}
modules => [emqx_bridge_monitor]}
],
{ok, {SupFlags, ChildSpecs}}.

View File

@ -1,21 +0,0 @@
.eunit
deps
*.o
*.beam
*.plt
erl_crash.dump
ebin/*.beam
rel
_build
.concrete/DEV_MODE
.rebar
.erlang.mk
data
ebin
emqx_bridge_mqtt.d
*.rendered
.rebar3/
*.coverdata
rebar.lock
.DS_Store
Mnesia.*/

View File

@ -1,265 +0,0 @@
# EMQ Bridge MQTT
The concept of **Bridge** means that EMQ X supports forwarding messages
of one of its own topics to another MQTT Broker in some way.
**Bridge** differs from **Cluster** in that the bridge does not
replicate the topic trie and routing tables and only forwards MQTT
messages based on bridging rules.
At present, the bridging methods supported by EMQ X are as follows:
- RPC bridge: RPC Bridge only supports message forwarding and does not
support subscribing to the topic of remote nodes to synchronize
data;
- MQTT Bridge: MQTT Bridge supports both forwarding and data
synchronization through subscription topic.
These concepts are shown below:
![bridge](docs/images/bridge.png)
In addition, the EMQ X message broker supports multi-node bridge mode interconnection
```
--------- --------- ---------
Publisher --> | Node1 | --Bridge Forward--> | Node2 | --Bridge Forward--> | Node3 | --> Subscriber
--------- --------- ---------
```
In EMQ X, bridge is configured by modifying `etc/emqx.conf`. EMQ X distinguishes between different bridges based on different names. E.g
```
## Bridge address: node name for local bridge, host:port for remote.
bridge.mqtt.aws.address = 127.0.0.1:1883
```
This configuration declares a bridge named `aws` and specifies that it is bridged to the MQTT broker of 127.0.0.1:1883 by MQTT mode.
In case of creating multiple bridges, it is convenient to replicate all configuration items of the first bridge, and modify the bridge name and other configuration items if necessary (such as bridge.$name.address, where $name refers to the name of bridge)
The next two sections describe how to create a bridge in RPC and MQTT mode respectively and create a forwarding rule that forwards the messages from sensors. Assuming that two EMQ X nodes are running on two hosts:
| Name | Node | MQTT Port |
|------|-------------------|-----------|
| emqx1| emqx1@192.168.1.1.| 1883 |
| emqx2| emqx2@192.168.1.2 | 1883 |
## EMQ X RPC Bridge Configuration
The following is the basic configuration of RPC bridging. A simplest RPC bridging only requires the following three items
```
## Bridge Address: Use node name (nodename@host) for rpc bridging, and host:port for mqtt connection
bridge.mqtt.emqx2.address = "emqx2@192.168.1.2"
## Forwarding topics of the message
bridge.mqtt.emqx2.forwards = "sensor1/#,sensor2/#"
## bridged mountpoint
bridge.mqtt.emqx2.mountpoint = "bridge/emqx2/${node}/"
```
If the messages received by the local node emqx1 matches the topic `sersor1/#` or `sensor2/#`, these messages will be forwarded to the `sensor1/#` or `sensor2/#` topic of the remote node emqx2.
`forwards` is used to specify topics. Messages of the in `forwards` specified topics on local node are forwarded to the remote node.
`mountpoint` is used to add a topic prefix when forwarding a message. To use `mountpoint`, the `forwards` directive must be set. In the above example, a message with the topic `sensor1/hello` received by the local node will be forwarded to the remote node with the topic `bridge/emqx2/emqx1@192.168.1.1/sensor1/hello`.
Limitations of RPC bridging:
1. The RPC bridge of emqx can only forward local messages to the remote node, and cannot synchronize the messages of the remote node to the local node;
2. RPC bridge can only bridge two EMQ X broker together and cannot bridge EMQ X broker to other MQTT brokers.
## EMQ X MQTT Bridge Configuration
EMQ X 3.0 officially introduced MQTT bridge, so that EMQ X can bridge any MQTT broker. Because of the characteristics of the MQTT protocol, EMQ X can subscribe to the remote mqtt broker's topic through MQTT bridge, and then synchronize the remote MQTT broker's message to the local.
EMQ X MQTT bridging principle: Create an MQTT client on the EMQ X broker, and connect this MQTT client to the remote MQTT broker. Therefore, in the MQTT bridge configuration, following fields may be set for the EMQ X to connect to the remote broker as an mqtt client
```
## Bridge Address: Use node name for rpc bridging, use host:port for mqtt connection
bridge.mqtt.emqx2.address = "192.168.1.2:1883"
## Bridged Protocol Version
## Enumeration value: mqttv3 | mqttv4 | mqttv5
bridge.mqtt.emqx2.proto_ver = "mqttv4"
## mqtt client's clientid
bridge.mqtt.emqx2.clientid = "bridge_emq"
## mqtt client's clean_start field
## Note: Some MQTT Brokers need to set the clean_start value as `true`
bridge.mqtt.emqx2.clean_start = true
## mqtt client's username field
bridge.mqtt.emqx2.username = "user"
## mqtt client's password field
bridge.mqtt.emqx2.password = "passwd"
## Whether the mqtt client uses ssl to connect to a remote serve or not
bridge.mqtt.emqx2.ssl = off
## CA Certificate of Client SSL Connection (PEM format)
bridge.mqtt.emqx2.cacertfile = "etc/certs/cacert.pem"
## SSL certificate of Client SSL connection
bridge.mqtt.emqx2.certfile = "etc/certs/client-cert.pem"
## Key file of Client SSL connection
bridge.mqtt.emqx2.keyfile = "etc/certs/client-key.pem"
## SSL encryption
bridge.mqtt.emqx2.ciphers = "ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES256-GCM-SHA384"
## TTLS PSK password
## Note 'listener.ssl.external.ciphers' and 'listener.ssl.external.psk_ciphers' cannot be configured at the same time
##
## See 'https://tools.ietf.org/html/rfc4279#section-2'.
## bridge.mqtt.emqx2.psk_ciphers = "PSK-AES128-CBC-SHA,PSK-AES256-CBC-SHA,PSK-3DES-EDE-CBC-SHA,PSK-RC4-SHA"
## Client's heartbeat interval
bridge.mqtt.emqx2.keepalive = 60s
## Supported TLS version
bridge.mqtt.emqx2.tls_versions = "tlsv1.3,tlsv1.2,tlsv1.1,tlsv1"
## Forwarding topics of the message
bridge.mqtt.emqx2.forwards = "sensor1/#,sensor2/#"
## Bridged mountpoint
bridge.mqtt.emqx2.mountpoint = "bridge/emqx2/${node}/"
## Subscription topic for bridging
bridge.mqtt.emqx2.subscription.1.topic = "cmd/topic1"
## Subscription qos for bridging
bridge.mqtt.emqx2.subscription.1.qos = 1
## Subscription topic for bridging
bridge.mqtt.emqx2.subscription.2.topic = "cmd/topic2"
## Subscription qos for bridging
bridge.mqtt.emqx2.subscription.2.qos = 1
## Bridging reconnection interval
## Default: 30s
bridge.mqtt.emqx2.reconnect_interval = 30s
## QoS1 message retransmission interval
bridge.mqtt.emqx2.retry_interval = 20s
## Inflight Size.
bridge.mqtt.emqx2.max_inflight_batches = 32
```
## Bridge Cache Configuration
The bridge of EMQ X has a message caching mechanism. The caching mechanism is applicable to both RPC bridging and MQTT bridging. When the bridge is disconnected (such as when the network connection is unstable), the messages with a topic specified in `forwards` can be cached to the local message queue. Until the bridge is restored, these messages are re-forwarded to the remote node. The configuration of the cache queue is as follows
```
## emqx_bridge internal number of messages used for batch
bridge.mqtt.emqx2.queue.batch_count_limit = 32
## emqx_bridge internal number of message bytes used for batch
bridge.mqtt.emqx2.queue.batch_bytes_limit = 1000MB
## The path for placing replayq queue. If it is not specified, then replayq will run in `mem-only` mode and messages will not be cached on disk.
bridge.mqtt.emqx2.queue.replayq_dir = data/emqx_emqx2_bridge/
## Replayq data segment size
bridge.mqtt.emqx2.queue.replayq_seg_bytes = 10MB
```
`bridge.mqtt.emqx2.queue.replayq_dir` is a configuration parameter for specifying the path of the bridge storage queue.
`bridge.mqtt.emqx2.queue.replayq_seg_bytes` is used to specify the size of the largest single file of the message queue that is cached on disk. If the message queue size exceeds the specified value, a new file is created to store the message queue.
## CLI for EMQ X Bridge MQTT
CLI for EMQ X Bridge MQTT:
``` bash
$ cd emqx1/ && ./bin/emqx_ctl bridges
bridges list # List bridges
bridges start <Name> # Start a bridge
bridges stop <Name> # Stop a bridge
bridges forwards <Name> # Show a bridge forward topic
bridges add-forward <Name> <Topic> # Add bridge forward topic
bridges del-forward <Name> <Topic> # Delete bridge forward topic
bridges subscriptions <Name> # Show a bridge subscriptions topic
bridges add-subscription <Name> <Topic> <Qos> # Add bridge subscriptions topic
```
List all bridge states
``` bash
$ ./bin/emqx_ctl bridges list
name: emqx status: Stopped $ ./bin/emqx_ctl bridges list
name: emqx status: Stopped
```
Start the specified bridge
``` bash
$ ./bin/emqx_ctl bridges start emqx
Start bridge successfully.
```
Stop the specified bridge
``` bash
$ ./bin/emqx_ctl bridges stop emqx
Stop bridge successfully.
```
List the forwarding topics for the specified bridge
``` bash
$ ./bin/emqx_ctl bridges forwards emqx
topic: topic1/#
topic: topic2/#
```
Add a forwarding topic for the specified bridge
``` bash
$ ./bin/emqx_ctl bridges add-forwards emqx topic3/#
Add-forward topic successfully.
```
Delete the forwarding topic for the specified bridge
``` bash
$ ./bin/emqx_ctl bridges del-forwards emqx topic3/#
Del-forward topic successfully.
```
List subscriptions for the specified bridge
``` bash
$ ./bin/emqx_ctl bridges subscriptions emqx
topic: cmd/topic1, qos: 1
topic: cmd/topic2, qos: 1
```
Add a subscription topic for the specified bridge
``` bash
$ ./bin/emqx_ctl bridges add-subscription emqx cmd/topic3 1
Add-subscription topic successfully.
```
Delete the subscription topic for the specified bridge
``` bash
$ ./bin/emqx_ctl bridges del-subscription emqx cmd/topic3
Del-subscription topic successfully.
```
Note: In case of creating multiple bridges, it is convenient to replicate all configuration items of the first bridge, and modify the bridge name and other configuration items if necessary.

View File

@ -1,286 +0,0 @@
EMQ Bridge MQTT
===============
The concept of **Bridge** means that EMQ X supports forwarding messages
of one of its own topics to another MQTT Broker in some way.
**Bridge** differs from **Cluster** in that the bridge does not
replicate the topic trie and routing tables and only forwards MQTT
messages based on bridging rules.
At present, the bridging methods supported by EMQ X are as follows:
* RPC bridge: RPC Bridge only supports message forwarding and does not
support subscribing to the topic of remote nodes to synchronize
data;
* MQTT Bridge: MQTT Bridge supports both forwarding and data
synchronization through subscription topic.
These concepts are shown below:
.. image:: images/bridge.png
:target: images/bridge.png
:alt: bridge
In addition, the EMQ X message broker supports multi-node bridge mode interconnection
.. code-block::
--------- --------- ---------
Publisher --> | Node1 | --Bridge Forward--> | Node2 | --Bridge Forward--> | Node3 | --> Subscriber
--------- --------- ---------
In EMQ X, bridge is configured by modifying ``etc/emqx.conf``. EMQ X distinguishes between different bridges based on different names. E.g
.. code-block::
## Bridge address: node name for local bridge, host:port for remote.
bridge.mqtt.aws.address = "127.0.0.1:1883"
This configuration declares a bridge named ``aws`` and specifies that it is bridged to the MQTT broker of 127.0.0.1:1883 by MQTT mode.
In case of creating multiple bridges, it is convenient to replicate all configuration items of the first bridge, and modify the bridge name and other configuration items if necessary (such as bridge.$name.address, where $name refers to the name of bridge)
The next two sections describe how to create a bridge in RPC and MQTT mode respectively and create a forwarding rule that forwards the messages from sensors. Assuming that two EMQ X nodes are running on two hosts:
.. list-table::
:header-rows: 1
* - Name
- Node
- MQTT Port
* - emqx1
- emqx1@192.168.1.1.
- 1883
* - emqx2
- emqx2@192.168.1.2
- 1883
EMQ X RPC Bridge Configuration
------------------------------
The following is the basic configuration of RPC bridging. A simplest RPC bridging only requires the following three items
.. code-block::
## Bridge Address: Use node name (nodename@host) for rpc bridging, and host:port for mqtt connection
bridge.mqtt.emqx2.address = "emqx2@192.168.1.2"
## Forwarding topics of the message
bridge.mqtt.emqx2.forwards = "sensor1/#,sensor2/#"
## bridged mountpoint
bridge.mqtt.emqx2.mountpoint = "bridge/emqx2/${node}/"
If the messages received by the local node emqx1 matches the topic ``sersor1/#`` or ``sensor2/#``\ , these messages will be forwarded to the ``sensor1/#`` or ``sensor2/#`` topic of the remote node emqx2.
``forwards`` is used to specify topics. Messages of the in ``forwards`` specified topics on local node are forwarded to the remote node.
``mountpoint`` is used to add a topic prefix when forwarding a message. To use ``mountpoint``\ , the ``forwards`` directive must be set. In the above example, a message with the topic ``sensor1/hello`` received by the local node will be forwarded to the remote node with the topic ``bridge/emqx2/emqx1@192.168.1.1/sensor1/hello``.
Limitations of RPC bridging:
#.
The RPC bridge of emqx can only forward local messages to the remote node, and cannot synchronize the messages of the remote node to the local node;
#.
RPC bridge can only bridge two EMQ X broker together and cannot bridge EMQ X broker to other MQTT brokers.
EMQ X MQTT Bridge Configuration
-------------------------------
EMQ X 3.0 officially introduced MQTT bridge, so that EMQ X can bridge any MQTT broker. Because of the characteristics of the MQTT protocol, EMQ X can subscribe to the remote mqtt broker's topic through MQTT bridge, and then synchronize the remote MQTT broker's message to the local.
EMQ X MQTT bridging principle: Create an MQTT client on the EMQ X broker, and connect this MQTT client to the remote MQTT broker. Therefore, in the MQTT bridge configuration, following fields may be set for the EMQ X to connect to the remote broker as an mqtt client
.. code-block::
## Bridge Address: Use node name for rpc bridging, use host:port for mqtt connection
bridge.mqtt.emqx2.address = "192.168.1.2:1883"
## Bridged Protocol Version
## Enumeration value: mqttv3 | mqttv4 | mqttv5
bridge.mqtt.emqx2.proto_ver = "mqttv4"
## mqtt client's clientid
bridge.mqtt.emqx2.clientid = "bridge_emq"
## mqtt client's clean_start field
## Note: Some MQTT Brokers need to set the clean_start value as `true`
bridge.mqtt.emqx2.clean_start = true
## mqtt client's username field
bridge.mqtt.emqx2.username = "user"
## mqtt client's password field
bridge.mqtt.emqx2.password = "passwd"
## Whether the mqtt client uses ssl to connect to a remote serve or not
bridge.mqtt.emqx2.ssl = off
## CA Certificate of Client SSL Connection (PEM format)
bridge.mqtt.emqx2.cacertfile = "etc/certs/cacert.pem"
## SSL certificate of Client SSL connection
bridge.mqtt.emqx2.certfile = "etc/certs/client-cert.pem"
## Key file of Client SSL connection
bridge.mqtt.emqx2.keyfile = "etc/certs/client-key.pem"
## TTLS PSK password
## Note 'listener.ssl.external.ciphers' and 'listener.ssl.external.psk_ciphers' cannot be configured at the same time
##
## See 'https://tools.ietf.org/html/rfc4279#section-2'.
## bridge.mqtt.emqx2.psk_ciphers = "PSK-AES128-CBC-SHA,PSK-AES256-CBC-SHA,PSK-3DES-EDE-CBC-SHA,PSK-RC4-SHA"
## Client's heartbeat interval
bridge.mqtt.emqx2.keepalive = 60s
## Supported TLS version
bridge.mqtt.emqx2.tls_versions = "tlsv1.2"
## SSL encryption
bridge.mqtt.emqx2.ciphers = "ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES256-GCM-SHA384"
## Forwarding topics of the message
bridge.mqtt.emqx2.forwards = "sensor1/#,sensor2/#"
## Bridged mountpoint
bridge.mqtt.emqx2.mountpoint = "bridge/emqx2/${node}/"
## Subscription topic for bridging
bridge.mqtt.emqx2.subscription.1.topic = "cmd/topic1"
## Subscription qos for bridging
bridge.mqtt.emqx2.subscription.1.qos = 1
## Subscription topic for bridging
bridge.mqtt.emqx2.subscription.2.topic = "cmd/topic2"
## Subscription qos for bridging
bridge.mqtt.emqx2.subscription.2.qos = 1
## Bridging reconnection interval
## Default: 30s
bridge.mqtt.emqx2.reconnect_interval = 30s
## QoS1 message retransmission interval
bridge.mqtt.emqx2.retry_interval = 20s
## Inflight Size.
bridge.mqtt.emqx2.max_inflight_batches = 32
Bridge Cache Configuration
--------------------------
The bridge of EMQ X has a message caching mechanism. The caching mechanism is applicable to both RPC bridging and MQTT bridging. When the bridge is disconnected (such as when the network connection is unstable), the messages with a topic specified in ``forwards`` can be cached to the local message queue. Until the bridge is restored, these messages are re-forwarded to the remote node. The configuration of the cache queue is as follows
.. code-block::
## emqx_bridge internal number of messages used for batch
bridge.mqtt.emqx2.queue.batch_count_limit = 32
## emqx_bridge internal number of message bytes used for batch
bridge.mqtt.emqx2.queue.batch_bytes_limit = 1000MB
## The path for placing replayq queue. If it is not specified, then replayq will run in `mem-only` mode and messages will not be cached on disk.
bridge.mqtt.emqx2.queue.replayq_dir = "data/emqx_emqx2_bridge/"
## Replayq data segment size
bridge.mqtt.emqx2.queue.replayq_seg_bytes = 10MB
``bridge.mqtt.emqx2.queue.replayq_dir`` is a configuration parameter for specifying the path of the bridge storage queue.
``bridge.mqtt.emqx2.queue.replayq_seg_bytes`` is used to specify the size of the largest single file of the message queue that is cached on disk. If the message queue size exceeds the specified value, a new file is created to store the message queue.
CLI for EMQ X Bridge MQTT
-------------------------
CLI for EMQ X Bridge MQTT:
.. code-block:: bash
$ cd emqx1/ && ./bin/emqx_ctl bridges
bridges list # List bridges
bridges start <Name> # Start a bridge
bridges stop <Name> # Stop a bridge
bridges forwards <Name> # Show a bridge forward topic
bridges add-forward <Name> <Topic> # Add bridge forward topic
bridges del-forward <Name> <Topic> # Delete bridge forward topic
bridges subscriptions <Name> # Show a bridge subscriptions topic
bridges add-subscription <Name> <Topic> <Qos> # Add bridge subscriptions topic
List all bridge states
.. code-block:: bash
$ ./bin/emqx_ctl bridges list
name: emqx status: Stopped $ ./bin/emqx_ctl bridges list
name: emqx status: Stopped
Start the specified bridge
.. code-block:: bash
$ ./bin/emqx_ctl bridges start emqx
Start bridge successfully.
Stop the specified bridge
.. code-block:: bash
$ ./bin/emqx_ctl bridges stop emqx
Stop bridge successfully.
List the forwarding topics for the specified bridge
.. code-block:: bash
$ ./bin/emqx_ctl bridges forwards emqx
topic: topic1/#
topic: topic2/#
Add a forwarding topic for the specified bridge
.. code-block:: bash
$ ./bin/emqx_ctl bridges add-forwards emqx topic3/#
Add-forward topic successfully.
Delete the forwarding topic for the specified bridge
.. code-block:: bash
$ ./bin/emqx_ctl bridges del-forwards emqx topic3/#
Del-forward topic successfully.
List subscriptions for the specified bridge
.. code-block:: bash
$ ./bin/emqx_ctl bridges subscriptions emqx
topic: cmd/topic1, qos: 1
topic: cmd/topic2, qos: 1
Add a subscription topic for the specified bridge
.. code-block:: bash
$ ./bin/emqx_ctl bridges add-subscription emqx cmd/topic3 1
Add-subscription topic successfully.
Delete the subscription topic for the specified bridge
.. code-block:: bash
$ ./bin/emqx_ctl bridges del-subscription emqx cmd/topic3
Del-subscription topic successfully.
Note: In case of creating multiple bridges, it is convenient to replicate all configuration items of the first bridge, and modify the bridge name and other configuration items if necessary.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

View File

@ -1,56 +0,0 @@
##====================================================================
## Configuration for EMQ X MQTT Broker Bridge
##====================================================================
bridge_mqtt: [
# {
# name: "mqtt1"
# start_type: auto
# forwards: ["test/#"],
# forward_mountpoint: ""
# reconnect_interval: "30s"
# batch_size: 100
# queue {
# replayq_dir: "{{ platform_data_dir }}/replayq/bridge_mqtt/"
# replayq_seg_bytes: "100MB"
# replayq_offload_mode: false
# replayq_max_total_bytes: "1GB"
# },
# config {
# conn_type: mqtt
# address: "127.0.0.1:1883"
# proto_ver: v4
# bridge_mode: true
# clientid: "client1"
# clean_start: true
# username: "username1"
# password: ""
# keepalive: 300
# subscriptions: [{
# topic: "t/#"
# qos: 1
# }]
# receive_mountpoint: ""
# retry_interval: "30s"
# max_inflight: 32
# }
# },
# {
# name: "rpc1"
# start_type: auto
# forwards: ["test/#"],
# forward_mountpoint: ""
# reconnect_interval: "30s"
# batch_size: 100
# queue {
# replayq_dir: "{{ platform_data_dir }}/replayq/bridge_mqtt/"
# replayq_seg_bytes: "100MB"
# replayq_offload_mode: false
# replayq_max_total_bytes: "1GB"
# },
# config {
# conn_type: rpc
# node: "emqx@127.0.0.1"
# }
# }
]

View File

@ -1,18 +0,0 @@
%%--------------------------------------------------------------------
%% 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.
%%--------------------------------------------------------------------
-define(APP, emqx_bridge_mqtt).

View File

@ -1,19 +0,0 @@
{deps, []}.
{edoc_opts, [{preprocess, true}]}.
{erl_opts, [warn_unused_vars,
warn_shadow_vars,
warn_unused_import,
warn_obsolete_guard,
debug_info]}.
{xref_checks, [undefined_function_calls, undefined_functions,
locals_not_used, deprecated_function_calls,
warnings_as_errors, deprecated_functions]}.
{cover_enabled, true}.
{cover_opts, [verbose]}.
{cover_export_enabled, true}.
{shell, [
% {config, "config/sys.config"},
{apps, [emqx, emqx_bridge_mqtt]}
]}.

View File

@ -1,14 +0,0 @@
{application, emqx_bridge_mqtt,
[{description, "EMQ X Bridge to MQTT Broker"},
{vsn, "5.0.0"}, % strict semver, bump manually!
{modules, []},
{registered, []},
{applications, [kernel,stdlib,replayq,emqtt,emqx]},
{mod, {emqx_bridge_mqtt_app, []}},
{env, []},
{licenses, ["Apache-2.0"]},
{maintainers, ["EMQ X Team <contact@emqx.io>"]},
{links, [{"Homepage", "https://emqx.io/"},
{"Github", "https://github.com/emqx/emqx-bridge-mqtt"}
]}
]}.

View File

@ -1,31 +0,0 @@
%%--------------------------------------------------------------------
%% 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_bridge_mqtt_app).
-behaviour(application).
-export([start/2, stop/1]).
start(_StartType, _StartArgs) ->
emqx_ctl:register_command(bridges, {emqx_bridge_mqtt_cli, cli}, []),
emqx_bridge_worker:register_metrics(),
emqx_bridge_mqtt_sup:start_link().
stop(_State) ->
emqx_ctl:unregister_command(bridges),
ok.

View File

@ -1,92 +0,0 @@
%%--------------------------------------------------------------------
%% 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_bridge_mqtt_cli).
-include("emqx_bridge_mqtt.hrl").
-import(lists, [foreach/2]).
-export([cli/1]).
cli(["list"]) ->
foreach(fun({Name, State0}) ->
State = case State0 of
connected -> <<"Running">>;
_ -> <<"Stopped">>
end,
emqx_ctl:print("name: ~s status: ~s~n", [Name, State])
end, emqx_bridge_mqtt_sup:bridges());
cli(["start", Name]) ->
emqx_ctl:print("~s.~n", [try emqx_bridge_worker:ensure_started(Name) of
ok -> <<"Start bridge successfully">>;
connected -> <<"Bridge already started">>;
_ -> <<"Start bridge failed">>
catch
_Error:_Reason ->
<<"Start bridge failed">>
end]);
cli(["stop", Name]) ->
emqx_ctl:print("~s.~n", [try emqx_bridge_worker:ensure_stopped(Name) of
ok -> <<"Stop bridge successfully">>;
_ -> <<"Stop bridge failed">>
catch
_Error:_Reason ->
<<"Stop bridge failed">>
end]);
cli(["forwards", Name]) ->
foreach(fun(Topic) ->
emqx_ctl:print("topic: ~s~n", [Topic])
end, emqx_bridge_worker:get_forwards(Name));
cli(["add-forward", Name, Topic]) ->
ok = emqx_bridge_worker:ensure_forward_present(Name, iolist_to_binary(Topic)),
emqx_ctl:print("Add-forward topic successfully.~n");
cli(["del-forward", Name, Topic]) ->
ok = emqx_bridge_worker:ensure_forward_absent(Name, iolist_to_binary(Topic)),
emqx_ctl:print("Del-forward topic successfully.~n");
cli(["subscriptions", Name]) ->
foreach(fun({Topic, Qos}) ->
emqx_ctl:print("topic: ~s, qos: ~p~n", [Topic, Qos])
end, emqx_bridge_worker:get_subscriptions(Name));
cli(["add-subscription", Name, Topic, Qos]) ->
case emqx_bridge_worker:ensure_subscription_present(Name, Topic, list_to_integer(Qos)) of
ok -> emqx_ctl:print("Add-subscription topic successfully.~n");
{error, Reason} -> emqx_ctl:print("Add-subscription failed reason: ~p.~n", [Reason])
end;
cli(["del-subscription", Name, Topic]) ->
ok = emqx_bridge_worker:ensure_subscription_absent(Name, Topic),
emqx_ctl:print("Del-subscription topic successfully.~n");
cli(_) ->
emqx_ctl:usage([{"bridges list", "List bridges"},
{"bridges start <Name>", "Start a bridge"},
{"bridges stop <Name>", "Stop a bridge"},
{"bridges forwards <Name>", "Show a bridge forward topic"},
{"bridges add-forward <Name> <Topic>", "Add bridge forward topic"},
{"bridges del-forward <Name> <Topic>", "Delete bridge forward topic"},
{"bridges subscriptions <Name>", "Show a bridge subscriptions topic"},
{"bridges add-subscription <Name> <Topic> <Qos>", "Add bridge subscriptions topic"},
{"bridges del-subscription <Name> <Topic>", "Delete bridge subscriptions topic"}]).

Some files were not shown because too many files have changed in this diff Show More