refactor(emqx_auth_mnesia): use tag e4.2.2
Removed emqx_auth_clientid and emqx_auth_username because the new version emqx_auth_mnesia has all the features included
This commit is contained in:
parent
5f0f91bb15
commit
e236196fa6
|
@ -1,117 +0,0 @@
|
||||||
emqx_auth_clientid
|
|
||||||
==================
|
|
||||||
|
|
||||||
Authentication with ClientId and Password
|
|
||||||
|
|
||||||
Build
|
|
||||||
-----
|
|
||||||
|
|
||||||
```
|
|
||||||
make && make tests
|
|
||||||
```
|
|
||||||
|
|
||||||
Configuration
|
|
||||||
-------------
|
|
||||||
|
|
||||||
etc/emqx_auth_clientid.conf:
|
|
||||||
|
|
||||||
```
|
|
||||||
## Password hash.
|
|
||||||
##
|
|
||||||
## Value: plain | md5 | sha | sha256
|
|
||||||
auth.client.password_hash = sha256
|
|
||||||
```
|
|
||||||
|
|
||||||
[REST API](https://developer.emqx.io/docs/emq/v3/en/rest.html)
|
|
||||||
------------
|
|
||||||
|
|
||||||
List all clientids:
|
|
||||||
|
|
||||||
```
|
|
||||||
# Request
|
|
||||||
GET api/v4/auth_clientid
|
|
||||||
|
|
||||||
# Response
|
|
||||||
{
|
|
||||||
"code": 0,
|
|
||||||
"data": ["clientid1"]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Add clientid:
|
|
||||||
|
|
||||||
```
|
|
||||||
# Request
|
|
||||||
POST api/v4/auth_clientid
|
|
||||||
{
|
|
||||||
"clientid": "a_client_id",
|
|
||||||
"password": "password"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Response
|
|
||||||
{
|
|
||||||
"code": 0
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Update password for a clientid:
|
|
||||||
|
|
||||||
```
|
|
||||||
# Request
|
|
||||||
PUT api/v4/auth_clientid/$CLIENTID
|
|
||||||
|
|
||||||
{
|
|
||||||
"password": "password"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Response
|
|
||||||
{
|
|
||||||
"code": 0
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Lookup a clientid info:
|
|
||||||
|
|
||||||
```
|
|
||||||
# Request
|
|
||||||
GET api/v4/auth_clientid/$CLIENTID
|
|
||||||
|
|
||||||
# Response
|
|
||||||
{
|
|
||||||
"code": 0,
|
|
||||||
"data": {
|
|
||||||
"clientid": "a_client_id",
|
|
||||||
"password": "hash_password"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Delete a clientid:
|
|
||||||
|
|
||||||
```
|
|
||||||
# Request
|
|
||||||
DELETE api/v4/auth_clientid/$CLIENTID
|
|
||||||
|
|
||||||
# Response
|
|
||||||
{
|
|
||||||
"code": 0
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Load the Plugin
|
|
||||||
---------------
|
|
||||||
|
|
||||||
```
|
|
||||||
./bin/emqx_ctl plugins load emqx_auth_clientid
|
|
||||||
```
|
|
||||||
|
|
||||||
License
|
|
||||||
-------
|
|
||||||
|
|
||||||
Apache License Version 2.0
|
|
||||||
|
|
||||||
Author
|
|
||||||
------
|
|
||||||
|
|
||||||
EMQ X Team.
|
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
1. Hash password
|
|
||||||
2. Add Test cases
|
|
||||||
3. Hot Reloader
|
|
|
@ -1,13 +0,0 @@
|
||||||
-define(APP, emqx_auth_clientid).
|
|
||||||
|
|
||||||
-record(auth_metrics, {
|
|
||||||
success = 'client.auth.success',
|
|
||||||
failure = 'client.auth.failure',
|
|
||||||
ignore = 'client.auth.ignore'
|
|
||||||
}).
|
|
||||||
|
|
||||||
-define(METRICS(Type), tl(tuple_to_list(#Type{}))).
|
|
||||||
-define(METRICS(Type, K), #Type{}#Type.K).
|
|
||||||
|
|
||||||
-define(AUTH_METRICS, ?METRICS(auth_metrics)).
|
|
||||||
-define(AUTH_METRICS(K), ?METRICS(auth_metrics, K)).
|
|
|
@ -1,26 +0,0 @@
|
||||||
%%-*- mode: erlang -*-
|
|
||||||
%% emqx_auth_clientid config mapping
|
|
||||||
|
|
||||||
{mapping, "auth.client.password_hash", "emqx_auth_clientid.password_hash", [
|
|
||||||
{default, sha256},
|
|
||||||
{datatype, {enum, [plain, md5, sha, sha256]}}
|
|
||||||
]}.
|
|
||||||
|
|
||||||
{mapping, "auth.client.$id.clientid", "emqx_auth_clientid.client_list", [
|
|
||||||
{datatype, string}
|
|
||||||
]}.
|
|
||||||
|
|
||||||
{mapping, "auth.client.$id.password", "emqx_auth_clientid.client_list", [
|
|
||||||
{datatype, string}
|
|
||||||
]}.
|
|
||||||
|
|
||||||
{translation, "emqx_auth_clientid.client_list", fun(Conf) ->
|
|
||||||
ClientList = cuttlefish_variable:filter_by_prefix("auth.client", Conf),
|
|
||||||
lists:foldl(
|
|
||||||
fun({["auth", "client", Id, "clientid"], ClientId}, AccIn) ->
|
|
||||||
[{ClientId, cuttlefish:conf_get("auth.client." ++ Id ++ ".password", Conf)} | AccIn];
|
|
||||||
(_, AccIn) ->
|
|
||||||
AccIn
|
|
||||||
end, [], ClientList)
|
|
||||||
end}.
|
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
{deps, []}.
|
|
|
@ -1,14 +0,0 @@
|
||||||
{application, emqx_auth_clientid,
|
|
||||||
[{description, "EMQ X Authentication with ClientId/Password"},
|
|
||||||
{vsn, "5.0.0"}, % strict semver, bump manually!
|
|
||||||
{modules, []},
|
|
||||||
{registered, [emqx_auth_clientid_sup]},
|
|
||||||
{applications, [kernel,stdlib,minirest,emqx_passwd,emqx_libs]},
|
|
||||||
{mod, {emqx_auth_clientid_app, []}},
|
|
||||||
{env, []},
|
|
||||||
{licenses, ["Apache-2.0"]},
|
|
||||||
{maintainers, ["EMQ X Team <contact@emqx.io>"]},
|
|
||||||
{links, [{"Homepage", "https://emqx.io/"},
|
|
||||||
{"Github", "https://github.com/emqx/emqx-auth-clientid"}
|
|
||||||
]}
|
|
||||||
]}.
|
|
|
@ -1,170 +0,0 @@
|
||||||
%%--------------------------------------------------------------------
|
|
||||||
%% Copyright (c) 2020 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_auth_clientid).
|
|
||||||
|
|
||||||
-include("emqx_auth_clientid.hrl").
|
|
||||||
|
|
||||||
-include_lib("emqx_libs/include/emqx.hrl").
|
|
||||||
|
|
||||||
%% CLI callbacks
|
|
||||||
-export([cli/1]).
|
|
||||||
|
|
||||||
%% APIs
|
|
||||||
-export([ add_clientid/2
|
|
||||||
, update_password/2
|
|
||||||
, lookup_clientid/1
|
|
||||||
, remove_clientid/1
|
|
||||||
, all_clientids/0
|
|
||||||
]).
|
|
||||||
|
|
||||||
-export([unwrap_salt/1]).
|
|
||||||
|
|
||||||
%% Auth callbacks
|
|
||||||
-export([ init/1
|
|
||||||
, register_metrics/0
|
|
||||||
, check/3
|
|
||||||
, description/0
|
|
||||||
]).
|
|
||||||
|
|
||||||
-define(TAB, ?MODULE).
|
|
||||||
|
|
||||||
-record(?TAB, {clientid, password}).
|
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
|
||||||
%% CLI
|
|
||||||
%%--------------------------------------------------------------------
|
|
||||||
|
|
||||||
cli(["list"]) ->
|
|
||||||
ClientIds = mnesia:dirty_all_keys(?TAB),
|
|
||||||
[emqx_ctl:print("~s~n", [ClientId]) || ClientId <- ClientIds];
|
|
||||||
|
|
||||||
cli(["add", ClientId, Password]) ->
|
|
||||||
Ok = add_clientid(iolist_to_binary(ClientId), iolist_to_binary(Password)),
|
|
||||||
emqx_ctl:print("~p~n", [Ok]);
|
|
||||||
|
|
||||||
cli(["update", ClientId, NewPassword]) ->
|
|
||||||
Ok = update_password(iolist_to_binary(ClientId), iolist_to_binary(NewPassword)),
|
|
||||||
emqx_ctl:print("~p~n", [Ok]);
|
|
||||||
|
|
||||||
cli(["del", ClientId]) ->
|
|
||||||
emqx_ctl:print("~p~n", [remove_clientid(iolist_to_binary(ClientId))]);
|
|
||||||
|
|
||||||
cli(_) ->
|
|
||||||
emqx_ctl:usage([{"clientid list", "List ClientId"},
|
|
||||||
{"clientid add <ClientId> <Password>", "Add ClientId"},
|
|
||||||
{"clientid update <Clientid> <NewPassword>", "Update Clientid"},
|
|
||||||
{"clientid del <ClientId>", "Delete ClientId"}]).
|
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
|
||||||
%% API
|
|
||||||
%%--------------------------------------------------------------------
|
|
||||||
%% @doc Add clientid with password
|
|
||||||
-spec(add_clientid(binary(), binary()) -> {atomic, ok} | {aborted, any()}).
|
|
||||||
add_clientid(ClientId, Password) ->
|
|
||||||
Client = #?TAB{clientid = ClientId, password = encrypted_data(Password)},
|
|
||||||
ret(mnesia:transaction(fun do_add_clientid/1, [Client])).
|
|
||||||
|
|
||||||
do_add_clientid(Client = #?TAB{clientid = ClientId}) ->
|
|
||||||
case mnesia:read(?TAB, ClientId) of
|
|
||||||
[] -> mnesia:write(Client);
|
|
||||||
[_|_] -> mnesia:abort(exitsted)
|
|
||||||
end.
|
|
||||||
|
|
||||||
%% @doc Update clientid with newpassword
|
|
||||||
-spec(update_password(binary(), binary()) -> {atomic, ok} | {aborted, any()}).
|
|
||||||
update_password(ClientId, NewPassword) ->
|
|
||||||
Client = #?TAB{clientid = ClientId, password = encrypted_data(NewPassword)},
|
|
||||||
ret(mnesia:transaction(fun do_update_password/1, [Client])).
|
|
||||||
|
|
||||||
do_update_password(Client = #?TAB{clientid = ClientId}) ->
|
|
||||||
case mnesia:read(?TAB, ClientId) of
|
|
||||||
[_|_] -> mnesia:write(Client);
|
|
||||||
[] -> mnesia:abort(noexitsted)
|
|
||||||
end.
|
|
||||||
|
|
||||||
%% @doc Lookup clientid
|
|
||||||
-spec(lookup_clientid(binary()) -> list(#?TAB{})).
|
|
||||||
lookup_clientid(ClientId) ->
|
|
||||||
mnesia:dirty_read(?TAB, ClientId).
|
|
||||||
|
|
||||||
%% @doc Lookup all clientids
|
|
||||||
-spec(all_clientids() -> list(binary())).
|
|
||||||
all_clientids() ->
|
|
||||||
mnesia:dirty_all_keys(?TAB).
|
|
||||||
|
|
||||||
%% @doc Remove clientid
|
|
||||||
-spec(remove_clientid(binary()) -> {atomic, ok} | {aborted, term()}).
|
|
||||||
remove_clientid(ClientId) ->
|
|
||||||
ret(mnesia:transaction(fun mnesia:delete/1, [{?TAB, ClientId}])).
|
|
||||||
|
|
||||||
unwrap_salt(<<_Salt:4/binary, HashPasswd/binary>>) ->
|
|
||||||
HashPasswd.
|
|
||||||
|
|
||||||
%% @private
|
|
||||||
ret({atomic, ok}) -> ok;
|
|
||||||
ret({aborted, Error}) -> {error, Error}.
|
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
|
||||||
%% Auth callbacks
|
|
||||||
%%--------------------------------------------------------------------
|
|
||||||
|
|
||||||
init(DefaultIds) ->
|
|
||||||
ok = ekka_mnesia:create_table(?TAB, [
|
|
||||||
{disc_copies, [node()]},
|
|
||||||
{attributes, record_info(fields, ?TAB)},
|
|
||||||
{storage_properties, [{ets, [{read_concurrency, true}]}]}]),
|
|
||||||
lists:foreach(fun add_default_clientid/1, DefaultIds),
|
|
||||||
ok = ekka_mnesia:copy_table(?TAB, disc_copies).
|
|
||||||
|
|
||||||
%% @private
|
|
||||||
add_default_clientid({ClientId, Password}) ->
|
|
||||||
add_clientid(iolist_to_binary(ClientId), iolist_to_binary(Password)).
|
|
||||||
|
|
||||||
register_metrics() ->
|
|
||||||
[emqx_metrics:ensure(MetricName) || MetricName <- ?AUTH_METRICS].
|
|
||||||
|
|
||||||
check(#{clientid := ClientId, password := Password}, AuthResult, #{hash_type := HashType}) ->
|
|
||||||
case mnesia:dirty_read(?TAB, ClientId) of
|
|
||||||
[] -> emqx_metrics:inc(?AUTH_METRICS(ignore));
|
|
||||||
[#?TAB{password = <<Salt:4/binary, Hash/binary>>}] ->
|
|
||||||
case Hash =:= hash(Password, Salt, HashType) of
|
|
||||||
true ->
|
|
||||||
emqx_metrics:inc(?AUTH_METRICS(success)),
|
|
||||||
{stop, AuthResult#{auth_result => success, anonymous => false}};
|
|
||||||
false ->
|
|
||||||
emqx_metrics:inc(?AUTH_METRICS(failure)),
|
|
||||||
{stop, AuthResult#{auth_result => not_authorized, anonymous => false}}
|
|
||||||
end
|
|
||||||
end.
|
|
||||||
|
|
||||||
description() ->
|
|
||||||
"ClientId Authentication Module".
|
|
||||||
|
|
||||||
encrypted_data(Password) ->
|
|
||||||
HashType = application:get_env(emqx_auth_clientid, password_hash, sha256),
|
|
||||||
SaltBin = salt(),
|
|
||||||
<<SaltBin/binary, (hash(Password, SaltBin, HashType))/binary>>.
|
|
||||||
|
|
||||||
hash(undefined, SaltBin, HashType) ->
|
|
||||||
hash(<<>>, SaltBin, HashType);
|
|
||||||
hash(Password, SaltBin, HashType) ->
|
|
||||||
emqx_passwd:hash(HashType, <<SaltBin/binary, Password/binary>>).
|
|
||||||
|
|
||||||
salt() ->
|
|
||||||
rand:seed(exsplus, erlang:timestamp()),
|
|
||||||
Salt = rand:uniform(16#ffffffff), <<Salt:32>>.
|
|
||||||
|
|
|
@ -1,127 +0,0 @@
|
||||||
%%--------------------------------------------------------------------
|
|
||||||
%% Copyright (c) 2020 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_auth_clientid_api).
|
|
||||||
|
|
||||||
-include("emqx_auth_clientid.hrl").
|
|
||||||
|
|
||||||
-export([ list/2
|
|
||||||
, lookup/2
|
|
||||||
, add/2
|
|
||||||
, update/2
|
|
||||||
, delete/2
|
|
||||||
]).
|
|
||||||
|
|
||||||
-import(proplists, [get_value/2]).
|
|
||||||
-import(minirest, [return/0, return/1]).
|
|
||||||
|
|
||||||
-rest_api(#{name => list_clientid,
|
|
||||||
method => 'GET',
|
|
||||||
path => "/auth_clientid",
|
|
||||||
func => list,
|
|
||||||
descr => "List available clientid in the cluster"
|
|
||||||
}).
|
|
||||||
|
|
||||||
-rest_api(#{name => lookup_clientid,
|
|
||||||
method => 'GET',
|
|
||||||
path => "/auth_clientid/:bin:clientid",
|
|
||||||
func => lookup,
|
|
||||||
descr => "Lookup clientid in the cluster"
|
|
||||||
}).
|
|
||||||
|
|
||||||
-rest_api(#{name => add_clientid,
|
|
||||||
method => 'POST',
|
|
||||||
path => "/auth_clientid",
|
|
||||||
func => add,
|
|
||||||
descr => "Add clientid in the cluster"
|
|
||||||
}).
|
|
||||||
|
|
||||||
-rest_api(#{name => update_clientid,
|
|
||||||
method => 'PUT',
|
|
||||||
path => "/auth_clientid/:bin:clientid",
|
|
||||||
func => update,
|
|
||||||
descr => "Update clientid in the cluster"
|
|
||||||
}).
|
|
||||||
|
|
||||||
-rest_api(#{name => delete_clientid,
|
|
||||||
method => 'DELETE',
|
|
||||||
path => "/auth_clientid/:bin:clientid",
|
|
||||||
func => delete,
|
|
||||||
descr => "Delete clientid in the cluster"
|
|
||||||
}).
|
|
||||||
|
|
||||||
list(_Bindings, _Params) ->
|
|
||||||
return({ok, emqx_auth_clientid:all_clientids()}).
|
|
||||||
|
|
||||||
lookup(#{clientid := ClientId}, _Params) ->
|
|
||||||
case emqx_auth_clientid:lookup_clientid(ClientId) of
|
|
||||||
[] -> return({error, not_found});
|
|
||||||
Auth -> return({ok, format(Auth)})
|
|
||||||
end.
|
|
||||||
|
|
||||||
add(_Bindings, Params) ->
|
|
||||||
ClientId = get_value(<<"clientid">>, Params),
|
|
||||||
Password = get_value(<<"password">>, Params),
|
|
||||||
case validate([clientid, password], [ClientId, Password]) of
|
|
||||||
ok ->
|
|
||||||
case emqx_auth_clientid:add_clientid(ClientId, Password) of
|
|
||||||
ok -> return();
|
|
||||||
Error -> return(Error)
|
|
||||||
end;
|
|
||||||
Error -> return(Error)
|
|
||||||
end.
|
|
||||||
|
|
||||||
update(#{clientid := ClientId}, Params) ->
|
|
||||||
Password = get_value(<<"password">>, Params),
|
|
||||||
case validate([password], [Password]) of
|
|
||||||
ok ->
|
|
||||||
case emqx_auth_clientid:update_password(ClientId, Password) of
|
|
||||||
ok -> return();
|
|
||||||
Error -> return(Error)
|
|
||||||
end;
|
|
||||||
Error -> return(Error)
|
|
||||||
end.
|
|
||||||
|
|
||||||
delete(#{clientid := ClientId}, _) ->
|
|
||||||
case emqx_auth_clientid:remove_clientid(ClientId) of
|
|
||||||
ok -> return();
|
|
||||||
Error -> return(Error)
|
|
||||||
end.
|
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
|
||||||
%% Interval Funcs
|
|
||||||
%%------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
format([{?APP, ClientId, Password}]) ->
|
|
||||||
#{clientid => ClientId,
|
|
||||||
password => emqx_auth_clientid:unwrap_salt(Password)}.
|
|
||||||
|
|
||||||
validate([], []) ->
|
|
||||||
ok;
|
|
||||||
validate([K|Keys], [V|Values]) ->
|
|
||||||
case validation(K, V) of
|
|
||||||
false -> {error, K};
|
|
||||||
true -> validate(Keys, Values)
|
|
||||||
end.
|
|
||||||
|
|
||||||
validation(clientid, V) when is_binary(V)
|
|
||||||
andalso byte_size(V) > 0 ->
|
|
||||||
true;
|
|
||||||
validation(password, V) when is_binary(V)
|
|
||||||
andalso byte_size(V) > 0 ->
|
|
||||||
true;
|
|
||||||
validation(_, _) ->
|
|
||||||
false.
|
|
|
@ -1,53 +0,0 @@
|
||||||
%%--------------------------------------------------------------------
|
|
||||||
%% Copyright (c) 2020 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_auth_clientid_app).
|
|
||||||
|
|
||||||
-include("emqx_auth_clientid.hrl").
|
|
||||||
|
|
||||||
-behaviour(application).
|
|
||||||
|
|
||||||
-emqx_plugin(auth).
|
|
||||||
|
|
||||||
-export([ start/2
|
|
||||||
, stop/1
|
|
||||||
]).
|
|
||||||
|
|
||||||
-behaviour(supervisor).
|
|
||||||
|
|
||||||
-export([init/1]).
|
|
||||||
|
|
||||||
start(_Type, _Args) ->
|
|
||||||
emqx_ctl:register_command(clientid, {?APP, cli}, []),
|
|
||||||
emqx_auth_clientid:register_metrics(),
|
|
||||||
HashType = application:get_env(?APP, password_hash, sha256),
|
|
||||||
Params = #{hash_type => HashType},
|
|
||||||
emqx:hook('client.authenticate', fun emqx_auth_clientid:check/3, [Params]),
|
|
||||||
DefaultIds = application:get_env(?APP, client_list, []),
|
|
||||||
ok = emqx_auth_clientid:init(DefaultIds),
|
|
||||||
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
|
|
||||||
|
|
||||||
stop(_State) ->
|
|
||||||
emqx:unhook('client.authenticate', fun emqx_auth_clientid:check/3),
|
|
||||||
emqx_ctl:unregister_command(clientid).
|
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
|
||||||
%% Dummy supervisor
|
|
||||||
%%--------------------------------------------------------------------
|
|
||||||
|
|
||||||
init([]) ->
|
|
||||||
{ok, { {one_for_all, 1, 10}, []} }.
|
|
||||||
|
|
|
@ -1,191 +0,0 @@
|
||||||
%% Copyright (c) 2020 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_auth_clientid_SUITE).
|
|
||||||
|
|
||||||
-compile(nowarn_export_all).
|
|
||||||
-compile(export_all).
|
|
||||||
|
|
||||||
-include_lib("emqx_libs/include/emqx.hrl").
|
|
||||||
-include_lib("eunit/include/eunit.hrl").
|
|
||||||
-include_lib("common_test/include/ct.hrl").
|
|
||||||
|
|
||||||
-import(emqx_ct_http, [ request_api/3
|
|
||||||
, request_api/5
|
|
||||||
, get_http_data/1
|
|
||||||
, create_default_app/0
|
|
||||||
, default_auth_header/0
|
|
||||||
]).
|
|
||||||
|
|
||||||
-define(HOST, "http://127.0.0.1:8081/").
|
|
||||||
-define(API_VERSION, "v4").
|
|
||||||
-define(BASE_PATH, "api").
|
|
||||||
|
|
||||||
-define(CLIENTID, <<"client_id_for_ct">>).
|
|
||||||
-define(PASSWORD, <<"password">>).
|
|
||||||
-define(NPASSWORD, <<"password1">>).
|
|
||||||
-define(USER, #{clientid => ?CLIENTID, zone => external}).
|
|
||||||
|
|
||||||
all() ->
|
|
||||||
emqx_ct:all(?MODULE).
|
|
||||||
|
|
||||||
init_per_suite(Config) ->
|
|
||||||
emqx_ct_helpers:start_apps([emqx_auth_clientid, emqx_management], fun set_special_configs/1),
|
|
||||||
create_default_app(),
|
|
||||||
Config.
|
|
||||||
|
|
||||||
end_per_suite(_Config) ->
|
|
||||||
emqx_ct_helpers:stop_apps([emqx_auth_clientid, emqx_management]).
|
|
||||||
|
|
||||||
set_special_configs(emqx) ->
|
|
||||||
application:set_env(emqx, allow_anonymous, true),
|
|
||||||
application:set_env(emqx, enable_acl_cache, false),
|
|
||||||
LoadedPluginPath = filename:join(["test", "emqx_SUITE_data", "loaded_plugins"]),
|
|
||||||
application:set_env(emqx, plugins_loaded_file,
|
|
||||||
emqx_ct_helpers:deps_path(emqx, LoadedPluginPath));
|
|
||||||
|
|
||||||
set_special_configs(_App) ->
|
|
||||||
ok.
|
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
|
||||||
%% Testcases
|
|
||||||
%%------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
t_managing(_) ->
|
|
||||||
clean_all_clientids(),
|
|
||||||
|
|
||||||
ok = emqx_auth_clientid:add_clientid(?CLIENTID, ?PASSWORD),
|
|
||||||
?assertNotEqual(emqx_auth_clientid:lookup_clientid(?CLIENTID), []),
|
|
||||||
|
|
||||||
{ok, #{auth_result := success,
|
|
||||||
anonymous := false}} = emqx_access_control:authenticate(?USER#{password => ?PASSWORD}),
|
|
||||||
|
|
||||||
{error, _} = emqx_access_control:authenticate(?USER#{password => ?NPASSWORD}),
|
|
||||||
|
|
||||||
emqx_auth_clientid:update_password(?CLIENTID, ?NPASSWORD),
|
|
||||||
{ok, #{auth_result := success,
|
|
||||||
anonymous := false}} = emqx_access_control:authenticate(?USER#{password => ?NPASSWORD}),
|
|
||||||
|
|
||||||
ok = emqx_auth_clientid:remove_clientid(?CLIENTID),
|
|
||||||
{ok, #{auth_result := success,
|
|
||||||
anonymous := true}} = emqx_access_control:authenticate(?USER#{password => ?PASSWORD}).
|
|
||||||
|
|
||||||
t_cli(_) ->
|
|
||||||
clean_all_clientids(),
|
|
||||||
|
|
||||||
HashType = application:get_env(emqx_auth_clientid, password_hash, sha256),
|
|
||||||
|
|
||||||
emqx_auth_clientid:cli(["add", ?CLIENTID, ?PASSWORD]),
|
|
||||||
[{_, ?CLIENTID, <<Salt:4/binary, Hash/binary>>}] = emqx_auth_clientid:lookup_clientid(?CLIENTID),
|
|
||||||
?assertEqual(Hash, emqx_passwd:hash(HashType, <<Salt/binary, ?PASSWORD/binary>>)),
|
|
||||||
|
|
||||||
emqx_auth_clientid:cli(["update", ?CLIENTID, ?NPASSWORD]),
|
|
||||||
[{_, ?CLIENTID, <<Salt1:4/binary, Hash1/binary>>}] = emqx_auth_clientid:lookup_clientid(?CLIENTID),
|
|
||||||
?assertEqual(Hash1, emqx_passwd:hash(HashType, <<Salt1/binary, ?NPASSWORD/binary>>)),
|
|
||||||
|
|
||||||
emqx_auth_clientid:cli(["del", ?CLIENTID]),
|
|
||||||
?assertEqual([], emqx_auth_clientid:lookup_clientid(?CLIENTID)),
|
|
||||||
|
|
||||||
emqx_auth_clientid:cli(["add", "user1", "pass1"]),
|
|
||||||
emqx_auth_clientid:cli(["add", "user2", "pass2"]),
|
|
||||||
?assertEqual(2, length(emqx_auth_clientid:cli(["list"]))),
|
|
||||||
|
|
||||||
emqx_auth_clientid:cli(usage).
|
|
||||||
|
|
||||||
t_rest_api(_Config) ->
|
|
||||||
clean_all_clientids(),
|
|
||||||
|
|
||||||
HashType = application:get_env(emqx_auth_clientid, password_hash, sha256),
|
|
||||||
|
|
||||||
{ok, Result} = request_http_rest_list(),
|
|
||||||
[] = get_http_data(Result),
|
|
||||||
|
|
||||||
{ok, _} = request_http_rest_add(?CLIENTID, ?PASSWORD),
|
|
||||||
{ok, Result1} = request_http_rest_lookup(?CLIENTID),
|
|
||||||
#{<<"password">> := Hash} = get_http_data(Result1),
|
|
||||||
[{_, ?CLIENTID, <<Salt:4/binary, Hash/binary>>}] = emqx_auth_clientid:lookup_clientid(?CLIENTID),
|
|
||||||
?assertEqual(Hash, emqx_passwd:hash(HashType, <<Salt/binary, ?PASSWORD/binary>>)),
|
|
||||||
|
|
||||||
{ok, _} = request_http_rest_update(?CLIENTID, ?NPASSWORD),
|
|
||||||
[{_, ?CLIENTID, <<Salt1:4/binary, Hash1/binary>>}] = emqx_auth_clientid:lookup_clientid(?CLIENTID),
|
|
||||||
?assertEqual(Hash1, emqx_passwd:hash(HashType, <<Salt1/binary, ?NPASSWORD/binary>>)),
|
|
||||||
|
|
||||||
{ok, _} = request_http_rest_delete(?CLIENTID),
|
|
||||||
?assertEqual([], emqx_auth_clientid:lookup_clientid(?CLIENTID)).
|
|
||||||
|
|
||||||
t_conf_not_override_existed(_) ->
|
|
||||||
clean_all_clientids(),
|
|
||||||
|
|
||||||
application:stop(emqx_auth_clientid),
|
|
||||||
application:set_env(emqx_auth_clientid, client_list, [{?CLIENTID, ?PASSWORD}]),
|
|
||||||
application:ensure_all_started(emqx_auth_clientid),
|
|
||||||
|
|
||||||
{ok, _} = emqx_access_control:authenticate(?USER#{password => ?PASSWORD}),
|
|
||||||
emqx_auth_clientid:cli(["update", ?CLIENTID, ?NPASSWORD]),
|
|
||||||
|
|
||||||
{error, _} = emqx_access_control:authenticate(?USER#{password => ?PASSWORD}),
|
|
||||||
{ok, _} = emqx_access_control:authenticate(?USER#{password => ?NPASSWORD}),
|
|
||||||
|
|
||||||
application:stop(emqx_auth_clientid),
|
|
||||||
application:ensure_all_started(emqx_auth_clientid),
|
|
||||||
{ok, _} = emqx_access_control:authenticate(?USER#{password => ?NPASSWORD}),
|
|
||||||
|
|
||||||
ct:pal("~p", [ets:tab2list(emqx_auth_clientid)]),
|
|
||||||
{ok, _} = request_http_rest_update(?CLIENTID, ?PASSWORD),
|
|
||||||
application:stop(emqx_auth_clientid),
|
|
||||||
application:ensure_all_started(emqx_auth_clientid),
|
|
||||||
ct:pal("~p", [ets:tab2list(emqx_auth_clientid)]),
|
|
||||||
{ok, _} = emqx_access_control:authenticate(?USER#{password => ?PASSWORD}).
|
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
|
||||||
%% Helpers
|
|
||||||
%%--------------------------------------------------------------------
|
|
||||||
|
|
||||||
clean_all_clientids() ->
|
|
||||||
[mnesia:dirty_delete({emqx_auth_clientid, Id})
|
|
||||||
|| Id <- mnesia:dirty_all_keys(emqx_auth_clientid)].
|
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
|
||||||
%% REST API Requests
|
|
||||||
|
|
||||||
request_http_rest_list() ->
|
|
||||||
request_api(get, uri(), default_auth_header()).
|
|
||||||
|
|
||||||
request_http_rest_lookup(ClientId) ->
|
|
||||||
request_api(get, uri([ClientId]), default_auth_header()).
|
|
||||||
|
|
||||||
request_http_rest_add(ClientId, Password) ->
|
|
||||||
Params = #{<<"clientid">> => ClientId, <<"password">> => Password},
|
|
||||||
request_api(post, uri(), [], default_auth_header(), Params).
|
|
||||||
|
|
||||||
request_http_rest_update(ClientId, Password) ->
|
|
||||||
Params = #{<<"password">> => Password},
|
|
||||||
request_api(put, uri([ClientId]), [], default_auth_header(), Params).
|
|
||||||
|
|
||||||
request_http_rest_delete(ClientId) ->
|
|
||||||
request_api(delete, uri([ClientId]), default_auth_header()).
|
|
||||||
|
|
||||||
%% @private
|
|
||||||
uri() -> uri([]).
|
|
||||||
uri(Parts) when is_list(Parts) ->
|
|
||||||
NParts = [b2l(E) || E <- Parts],
|
|
||||||
%% http://127.0.0.1:8080/api/v4/auth_clientid/<P1>/<P2>/<Pn>
|
|
||||||
?HOST ++ filename:join([?BASE_PATH, ?API_VERSION, "auth_clientid"| NParts]).
|
|
||||||
|
|
||||||
%% @private
|
|
||||||
b2l(B) when is_binary(B) ->
|
|
||||||
binary_to_list(B);
|
|
||||||
b2l(L) when is_list(L) ->
|
|
||||||
L.
|
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
## Password hash.
|
||||||
|
##
|
||||||
|
## Value: plain | md5 | sha | sha256 | sha512
|
||||||
|
auth.mnesia.password_hash = sha256
|
||||||
|
|
||||||
|
##--------------------------------------------------------------------
|
||||||
|
## ClientId Authentication
|
||||||
|
##--------------------------------------------------------------------
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
##auth.client.1.clientid = id
|
||||||
|
##auth.client.1.password = passwd
|
||||||
|
##auth.client.2.clientid = dev:devid
|
||||||
|
##auth.client.2.password = passwd2
|
||||||
|
##auth.client.3.clientid = app:appid
|
||||||
|
##auth.client.3.password = passwd3
|
||||||
|
##auth.client.4.clientid = client~!@#$%^&*()_+
|
||||||
|
##auth.client.4.password = passwd~!@#$%^&*()_+
|
||||||
|
|
||||||
|
##--------------------------------------------------------------------
|
||||||
|
## Username Authentication
|
||||||
|
##--------------------------------------------------------------------
|
||||||
|
|
||||||
|
## Examples:
|
||||||
|
##auth.user.1.username = admin
|
||||||
|
##auth.user.1.password = public
|
||||||
|
##auth.user.2.username = feng@emqtt.io
|
||||||
|
##auth.user.2.password = public
|
||||||
|
##auth.user.3.username = name~!@#$%^&*()_+
|
||||||
|
##auth.user.3.password = pwsswd~!@#$%^&*()_+
|
|
@ -1,17 +1,20 @@
|
||||||
-define(APP, emqx_auth_mnesia).
|
-define(APP, emqx_auth_mnesia).
|
||||||
|
|
||||||
|
-type(login():: {clientid, binary()}
|
||||||
|
| {username, binary()}).
|
||||||
|
|
||||||
-record(emqx_user, {
|
-record(emqx_user, {
|
||||||
login,
|
login :: login(),
|
||||||
password,
|
password :: binary(),
|
||||||
is_superuser
|
created_at :: integer()
|
||||||
}).
|
}).
|
||||||
|
|
||||||
-record(emqx_acl, {
|
-record(emqx_acl, {
|
||||||
login,
|
filter:: {login() | all, emqx_topic:topic()},
|
||||||
topic,
|
action :: pub | sub | pubsub,
|
||||||
action,
|
access :: allow | deny,
|
||||||
allow
|
created_at :: integer()
|
||||||
}).
|
}).
|
||||||
|
|
||||||
-record(auth_metrics, {
|
-record(auth_metrics, {
|
||||||
success = 'client.auth.success',
|
success = 'client.auth.success',
|
||||||
|
@ -32,4 +35,4 @@
|
||||||
-define(AUTH_METRICS(K), ?METRICS(auth_metrics, K)).
|
-define(AUTH_METRICS(K), ?METRICS(auth_metrics, K)).
|
||||||
|
|
||||||
-define(ACL_METRICS, ?METRICS(acl_metrics)).
|
-define(ACL_METRICS, ?METRICS(acl_metrics)).
|
||||||
-define(ACL_METRICS(K), ?METRICS(acl_metrics, K)).
|
-define(ACL_METRICS(K), ?METRICS(acl_metrics, K)).
|
||||||
|
|
|
@ -1,34 +1,42 @@
|
||||||
%%-*- mode: erlang -*-
|
%%-*- mode: erlang -*-
|
||||||
%% emqx_auth_mnesia config mapping
|
%% emqx_auth_mnesia config mapping
|
||||||
|
|
||||||
{mapping, "auth.mnesia.as", "emqx_auth_mnesia.as", [
|
|
||||||
{default, username},
|
|
||||||
{datatype, {enum, [username, clientid]}}
|
|
||||||
]}.
|
|
||||||
|
|
||||||
{mapping, "auth.mnesia.password_hash", "emqx_auth_mnesia.password_hash", [
|
{mapping, "auth.mnesia.password_hash", "emqx_auth_mnesia.password_hash", [
|
||||||
{default, sha256},
|
{default, sha256},
|
||||||
{datatype, {enum, [plain, md5, sha, sha256]}}
|
{datatype, {enum, [plain, md5, sha, sha256, sha512]}}
|
||||||
]}.
|
]}.
|
||||||
|
|
||||||
{mapping, "auth.mnesia.$id.login", "emqx_auth_mnesia.userlist", [
|
{mapping, "auth.client.$id.clientid", "emqx_auth_mnesia.clientid_list", [
|
||||||
{datatype, string}
|
{datatype, string}
|
||||||
]}.
|
]}.
|
||||||
|
|
||||||
{mapping, "auth.mnesia.$id.password", "emqx_auth_mnesia.userlist", [
|
{mapping, "auth.client.$id.password", "emqx_auth_mnesia.clientid_list", [
|
||||||
{datatype, string}
|
{datatype, string}
|
||||||
]}.
|
]}.
|
||||||
|
|
||||||
{mapping, "auth.mnesia.$id.is_superuser", "emqx_auth_mnesia.userlist", [
|
{translation, "emqx_auth_mnesia.clientid_list", fun(Conf) ->
|
||||||
{default, false},
|
ClientList = cuttlefish_variable:filter_by_prefix("auth.client", Conf),
|
||||||
{datatype, {enum, [false, true]}}
|
|
||||||
]}.
|
|
||||||
|
|
||||||
{translation, "emqx_auth_mnesia.userlist", fun(Conf) ->
|
|
||||||
Userlist = cuttlefish_variable:filter_by_prefix("auth.mnesia", Conf),
|
|
||||||
lists:foldl(
|
lists:foldl(
|
||||||
fun({["auth", "mnesia", Id, "login"], Username}, AccIn) ->
|
fun({["auth", "client", Id, "clientid"], ClientId}, AccIn) ->
|
||||||
[{Username, cuttlefish:conf_get("auth.mnesia." ++ Id ++ ".password", Conf), cuttlefish:conf_get("auth.mnesia." ++ Id ++ ".is_superuser", Conf)} | AccIn];
|
[{ClientId, cuttlefish:conf_get("auth.client." ++ Id ++ ".password", Conf)} | AccIn];
|
||||||
|
(_, AccIn) ->
|
||||||
|
AccIn
|
||||||
|
end, [], ClientList)
|
||||||
|
end}.
|
||||||
|
|
||||||
|
{mapping, "auth.user.$id.username", "emqx_auth_mnesia.username_list", [
|
||||||
|
{datatype, string}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{mapping, "auth.user.$id.password", "emqx_auth_mnesia.username_list", [
|
||||||
|
{datatype, string}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{translation, "emqx_auth_mnesia.username_list", fun(Conf) ->
|
||||||
|
Userlist = cuttlefish_variable:filter_by_prefix("auth.user", Conf),
|
||||||
|
lists:foldl(
|
||||||
|
fun({["auth", "user", Id, "username"], Username}, AccIn) ->
|
||||||
|
[{Username, cuttlefish:conf_get("auth.user." ++ Id ++ ".password", Conf)} | AccIn];
|
||||||
(_, AccIn) ->
|
(_, AccIn) ->
|
||||||
AccIn
|
AccIn
|
||||||
end, [], Userlist)
|
end, [], Userlist)
|
||||||
|
|
|
@ -1 +1,29 @@
|
||||||
{deps, []}.
|
{minimum_otp_vsn, "21"}.
|
||||||
|
|
||||||
|
{deps,
|
||||||
|
[{emqx_passwd, {git, "https://github.com/emqx/emqx-passwd.git", {tag, "v1.1.1"}}},
|
||||||
|
{minirest, {git, "https://github.com/emqx/minirest.git", {tag, "0.3.0"}}}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{profiles,
|
||||||
|
[{test,
|
||||||
|
[{deps,
|
||||||
|
[{emqx_ct_helpers, {git, "https://github.com/emqx/emqx-ct-helpers", {tag, "v1.2.2"}}}
|
||||||
|
]}
|
||||||
|
]}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{erl_opts, [warn_unused_vars,
|
||||||
|
warn_shadow_vars,
|
||||||
|
warn_unused_import,
|
||||||
|
warn_obsolete_guard,
|
||||||
|
debug_info,
|
||||||
|
{parse_transform}]}.
|
||||||
|
|
||||||
|
{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}.
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,10 @@
|
||||||
|
|
||||||
-include("emqx_auth_mnesia.hrl").
|
-include("emqx_auth_mnesia.hrl").
|
||||||
|
|
||||||
|
-include_lib("stdlib/include/ms_transform.hrl").
|
||||||
|
|
||||||
|
-define(TABLE, emqx_acl).
|
||||||
|
|
||||||
%% ACL Callbacks
|
%% ACL Callbacks
|
||||||
-export([ init/0
|
-export([ init/0
|
||||||
, register_metrics/0
|
, register_metrics/0
|
||||||
|
@ -27,22 +31,38 @@
|
||||||
|
|
||||||
init() ->
|
init() ->
|
||||||
ok = ekka_mnesia:create_table(emqx_acl, [
|
ok = ekka_mnesia:create_table(emqx_acl, [
|
||||||
{type, bag},
|
|
||||||
{disc_copies, [node()]},
|
{disc_copies, [node()]},
|
||||||
{attributes, record_info(fields, emqx_acl)},
|
{attributes, record_info(fields, emqx_acl)},
|
||||||
{storage_properties, [{ets, [{read_concurrency, true}]}]}]),
|
{storage_properties, [{ets, [{read_concurrency, true}]}]}]),
|
||||||
ok = ekka_mnesia:copy_table(emqx_user, disc_copies).
|
ok = ekka_mnesia:copy_table(emqx_acl, disc_copies).
|
||||||
|
|
||||||
-spec(register_metrics() -> ok).
|
-spec(register_metrics() -> ok).
|
||||||
register_metrics() ->
|
register_metrics() ->
|
||||||
lists:foreach(fun emqx_metrics:ensure/1, ?ACL_METRICS).
|
lists:foreach(fun emqx_metrics:ensure/1, ?ACL_METRICS).
|
||||||
|
|
||||||
check_acl(ClientInfo, PubSub, Topic, NoMatchAction, #{key_as := As}) ->
|
check_acl(ClientInfo = #{ clientid := Clientid }, PubSub, Topic, _NoMatchAction, _Params) ->
|
||||||
Login = maps:get(As, ClientInfo),
|
Username = maps:get(username, ClientInfo, undefined),
|
||||||
case do_check_acl(Login, PubSub, Topic, NoMatchAction) of
|
|
||||||
ok -> emqx_metrics:inc(?ACL_METRICS(ignore)), ok;
|
Acls = case Username of
|
||||||
{stop, allow} -> emqx_metrics:inc(?ACL_METRICS(allow)), {stop, allow};
|
undefined ->
|
||||||
{stop, deny} -> emqx_metrics:inc(?ACL_METRICS(deny)), {stop, deny}
|
emqx_acl_mnesia_cli:lookup_acl({clientid, Clientid}) ++
|
||||||
|
emqx_acl_mnesia_cli:lookup_acl(all);
|
||||||
|
_ ->
|
||||||
|
emqx_acl_mnesia_cli:lookup_acl({clientid, Clientid}) ++
|
||||||
|
emqx_acl_mnesia_cli:lookup_acl({username, Username}) ++
|
||||||
|
emqx_acl_mnesia_cli:lookup_acl(all)
|
||||||
|
end,
|
||||||
|
|
||||||
|
case match(ClientInfo, PubSub, Topic, Acls) of
|
||||||
|
allow ->
|
||||||
|
emqx_metrics:inc(?ACL_METRICS(allow)),
|
||||||
|
{stop, allow};
|
||||||
|
deny ->
|
||||||
|
emqx_metrics:inc(?ACL_METRICS(deny)),
|
||||||
|
{stop, deny};
|
||||||
|
_ ->
|
||||||
|
emqx_metrics:inc(?ACL_METRICS(ignore)),
|
||||||
|
ok
|
||||||
end.
|
end.
|
||||||
|
|
||||||
description() -> "Acl with Mnesia".
|
description() -> "Acl with Mnesia".
|
||||||
|
@ -51,33 +71,33 @@ description() -> "Acl with Mnesia".
|
||||||
%% Internal functions
|
%% Internal functions
|
||||||
%%-------------------------------------------------------------------
|
%%-------------------------------------------------------------------
|
||||||
|
|
||||||
do_check_acl(Login, PubSub, Topic, _NoMatchAction) ->
|
match(_ClientInfo, _PubSub, _Topic, []) ->
|
||||||
case match(PubSub, Topic, emqx_auth_mnesia_cli:lookup_acl(Login)) of
|
|
||||||
allow -> {stop, allow};
|
|
||||||
deny -> {stop, deny};
|
|
||||||
_ ->
|
|
||||||
case match(PubSub, Topic, emqx_auth_mnesia_cli:lookup_acl(<<"$all">>)) of
|
|
||||||
allow -> {stop, allow};
|
|
||||||
deny -> {stop, deny};
|
|
||||||
_ -> ok
|
|
||||||
end
|
|
||||||
end.
|
|
||||||
|
|
||||||
match(_PubSub, _Topic, []) ->
|
|
||||||
nomatch;
|
nomatch;
|
||||||
match(PubSub, Topic, [ #emqx_acl{topic = ACLTopic, action = Action, allow = Allow} | UserAcl]) ->
|
match(ClientInfo, PubSub, Topic, [ {_, ACLTopic, Action, Access, _} | Acls]) ->
|
||||||
case match_actions(PubSub, Action) andalso match_topic(Topic, ACLTopic) of
|
case match_actions(PubSub, Action) andalso match_topic(ClientInfo, Topic, ACLTopic) of
|
||||||
true -> case Allow of
|
true -> Access;
|
||||||
true -> allow;
|
false -> match(ClientInfo, PubSub, Topic, Acls)
|
||||||
_ -> deny
|
|
||||||
end;
|
|
||||||
false -> match(PubSub, Topic, UserAcl)
|
|
||||||
end.
|
end.
|
||||||
|
|
||||||
match_topic(Topic, ACLTopic) when is_binary(Topic) ->
|
match_topic(ClientInfo, Topic, ACLTopic) when is_binary(Topic) ->
|
||||||
emqx_topic:match(Topic, ACLTopic).
|
emqx_topic:match(Topic, feed_var(ClientInfo, ACLTopic)).
|
||||||
|
|
||||||
match_actions(_, <<"pubsub">>) -> true;
|
match_actions(_, pubsub) -> true;
|
||||||
match_actions(subscribe, <<"sub">>) -> true;
|
match_actions(subscribe, sub) -> true;
|
||||||
match_actions(publish, <<"pub">>) -> true;
|
match_actions(publish, pub) -> true;
|
||||||
match_actions(_, _) -> false.
|
match_actions(_, _) -> false.
|
||||||
|
|
||||||
|
feed_var(ClientInfo, Pattern) ->
|
||||||
|
feed_var(ClientInfo, emqx_topic:words(Pattern), []).
|
||||||
|
feed_var(_ClientInfo, [], Acc) ->
|
||||||
|
emqx_topic:join(lists:reverse(Acc));
|
||||||
|
feed_var(ClientInfo = #{clientid := undefined}, [<<"%c">>|Words], Acc) ->
|
||||||
|
feed_var(ClientInfo, Words, [<<"%c">>|Acc]);
|
||||||
|
feed_var(ClientInfo = #{clientid := ClientId}, [<<"%c">>|Words], Acc) ->
|
||||||
|
feed_var(ClientInfo, Words, [ClientId |Acc]);
|
||||||
|
feed_var(ClientInfo = #{username := undefined}, [<<"%u">>|Words], Acc) ->
|
||||||
|
feed_var(ClientInfo, Words, [<<"%u">>|Acc]);
|
||||||
|
feed_var(ClientInfo = #{username := Username}, [<<"%u">>|Words], Acc) ->
|
||||||
|
feed_var(ClientInfo, Words, [Username|Acc]);
|
||||||
|
feed_var(ClientInfo, [W|Words], Acc) ->
|
||||||
|
feed_var(ClientInfo, Words, [W|Acc]).
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
%%--------------------------------------------------------------------
|
%c%--------------------------------------------------------------------
|
||||||
%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved.
|
%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
%%
|
%%
|
||||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
@ -18,94 +18,174 @@
|
||||||
|
|
||||||
-include("emqx_auth_mnesia.hrl").
|
-include("emqx_auth_mnesia.hrl").
|
||||||
|
|
||||||
-import(proplists, [get_value/2]).
|
-include_lib("stdlib/include/ms_transform.hrl").
|
||||||
|
|
||||||
|
-import(proplists, [ get_value/2
|
||||||
|
, get_value/3
|
||||||
|
]).
|
||||||
|
|
||||||
-import(minirest, [return/1]).
|
-import(minirest, [return/1]).
|
||||||
|
|
||||||
-rest_api(#{name => list_emqx_acl,
|
-rest_api(#{name => list_clientid,
|
||||||
method => 'GET',
|
method => 'GET',
|
||||||
path => "/mqtt_acl",
|
path => "/acl/clientid",
|
||||||
func => list,
|
func => list_clientid,
|
||||||
descr => "List available mnesia in the cluster"
|
descr => "List available mnesia in the cluster"
|
||||||
}).
|
}).
|
||||||
|
|
||||||
-rest_api(#{name => lookup_emqx_acl,
|
-rest_api(#{name => list_username,
|
||||||
method => 'GET',
|
method => 'GET',
|
||||||
path => "/mqtt_acl/:bin:login",
|
path => "/acl/username",
|
||||||
|
func => list_username,
|
||||||
|
descr => "List available mnesia in the cluster"
|
||||||
|
}).
|
||||||
|
|
||||||
|
-rest_api(#{name => list_all,
|
||||||
|
method => 'GET',
|
||||||
|
path => "/acl/$all",
|
||||||
|
func => list_all,
|
||||||
|
descr => "List available mnesia in the cluster"
|
||||||
|
}).
|
||||||
|
|
||||||
|
-rest_api(#{name => lookup_clientid,
|
||||||
|
method => 'GET',
|
||||||
|
path => "/acl/clientid/:bin:clientid",
|
||||||
func => lookup,
|
func => lookup,
|
||||||
descr => "Lookup mnesia in the cluster"
|
descr => "Lookup mnesia in the cluster"
|
||||||
}).
|
}).
|
||||||
|
|
||||||
-rest_api(#{name => add_emqx_acl,
|
-rest_api(#{name => lookup_username,
|
||||||
|
method => 'GET',
|
||||||
|
path => "/acl/username/:bin:username",
|
||||||
|
func => lookup,
|
||||||
|
descr => "Lookup mnesia in the cluster"
|
||||||
|
}).
|
||||||
|
|
||||||
|
-rest_api(#{name => add,
|
||||||
method => 'POST',
|
method => 'POST',
|
||||||
path => "/mqtt_acl",
|
path => "/acl",
|
||||||
func => add,
|
func => add,
|
||||||
descr => "Add mnesia in the cluster"
|
descr => "Add mnesia in the cluster"
|
||||||
}).
|
}).
|
||||||
|
|
||||||
-rest_api(#{name => delete_emqx_acl,
|
-rest_api(#{name => delete_clientid,
|
||||||
method => 'DELETE',
|
method => 'DELETE',
|
||||||
path => "/mqtt_acl/:bin:login/:bin:topic",
|
path => "/acl/clientid/:bin:clientid/topic/:bin:topic",
|
||||||
func => delete,
|
func => delete,
|
||||||
descr => "Delete mnesia in the cluster"
|
descr => "Delete mnesia in the cluster"
|
||||||
}).
|
}).
|
||||||
|
|
||||||
-export([ list/2
|
-rest_api(#{name => delete_username,
|
||||||
|
method => 'DELETE',
|
||||||
|
path => "/acl/username/:bin:username/topic/:bin:topic",
|
||||||
|
func => delete,
|
||||||
|
descr => "Delete mnesia in the cluster"
|
||||||
|
}).
|
||||||
|
|
||||||
|
-rest_api(#{name => delete_all,
|
||||||
|
method => 'DELETE',
|
||||||
|
path => "/acl/$all/topic/:bin:topic",
|
||||||
|
func => delete,
|
||||||
|
descr => "Delete mnesia in the cluster"
|
||||||
|
}).
|
||||||
|
|
||||||
|
|
||||||
|
-export([ list_clientid/2
|
||||||
|
, list_username/2
|
||||||
|
, list_all/2
|
||||||
, lookup/2
|
, lookup/2
|
||||||
, add/2
|
, add/2
|
||||||
, delete/2
|
, delete/2
|
||||||
]).
|
]).
|
||||||
|
|
||||||
list(_Bindings, Params) ->
|
list_clientid(_Bindings, Params) ->
|
||||||
return({ok, emqx_auth_mnesia_api:paginate(emqx_acl, Params, fun format/1)}).
|
MatchSpec = ets:fun2ms(
|
||||||
|
fun({emqx_acl, {{clientid, Clientid}, Topic}, Action, Access, CreatedAt}) -> {{clientid,Clientid}, Topic, Action,Access, CreatedAt} end),
|
||||||
|
return({ok, emqx_auth_mnesia_api:paginate(emqx_acl, MatchSpec, Params, fun emqx_acl_mnesia_cli:comparing/2, fun format/1)}).
|
||||||
|
|
||||||
lookup(#{login := Login}, _Params) ->
|
list_username(_Bindings, Params) ->
|
||||||
return({ok, format(emqx_auth_mnesia_cli:lookup_acl(urldecode(Login)))}).
|
MatchSpec = ets:fun2ms(
|
||||||
|
fun({emqx_acl, {{username, Username}, Topic}, Action, Access, CreatedAt}) -> {{username, Username}, Topic, Action,Access, CreatedAt} end),
|
||||||
|
return({ok, emqx_auth_mnesia_api:paginate(emqx_acl, MatchSpec, Params, fun emqx_acl_mnesia_cli:comparing/2, fun format/1)}).
|
||||||
|
|
||||||
|
list_all(_Bindings, Params) ->
|
||||||
|
MatchSpec = ets:fun2ms(
|
||||||
|
fun({emqx_acl, {all, Topic}, Action, Access, CreatedAt}) -> {all, Topic, Action,Access, CreatedAt}end
|
||||||
|
),
|
||||||
|
return({ok, emqx_auth_mnesia_api:paginate(emqx_acl, MatchSpec, Params, fun emqx_acl_mnesia_cli:comparing/2, fun format/1)}).
|
||||||
|
|
||||||
|
|
||||||
|
lookup(#{clientid := Clientid}, _Params) ->
|
||||||
|
return({ok, format(emqx_acl_mnesia_cli:lookup_acl({clientid, urldecode(Clientid)}))});
|
||||||
|
lookup(#{username := Username}, _Params) ->
|
||||||
|
return({ok, format(emqx_acl_mnesia_cli:lookup_acl({username, urldecode(Username)}))}).
|
||||||
|
|
||||||
add(_Bindings, Params) ->
|
add(_Bindings, Params) ->
|
||||||
[ P | _] = Params,
|
[ P | _] = Params,
|
||||||
case is_list(P) of
|
case is_list(P) of
|
||||||
true -> return(add_acl(Params, []));
|
true -> return(do_add(Params, []));
|
||||||
false -> return(add_acl([Params], []))
|
false ->
|
||||||
|
Re = do_add(Params),
|
||||||
|
case Re of
|
||||||
|
#{result := ok} -> return({ok, Re});
|
||||||
|
#{result := <<"ok">>} -> return({ok, Re});
|
||||||
|
_ -> return({error, Re})
|
||||||
|
end
|
||||||
end.
|
end.
|
||||||
|
|
||||||
add_acl([ Params | ParamsN ], ReList ) ->
|
do_add([ Params | ParamsN ], ReList) ->
|
||||||
Login = urldecode(get_value(<<"login">>, Params)),
|
do_add(ParamsN, [do_add(Params) | ReList]);
|
||||||
Topic = urldecode(get_value(<<"topic">>, Params)),
|
|
||||||
Action = urldecode(get_value(<<"action">>, Params)),
|
do_add([], ReList) ->
|
||||||
Allow = get_value(<<"allow">>, Params),
|
|
||||||
Re = case validate([login, topic, action, allow], [Login, Topic, Action, Allow]) of
|
|
||||||
ok ->
|
|
||||||
emqx_auth_mnesia_cli:add_acl(Login, Topic, Action, Allow);
|
|
||||||
Err -> Err
|
|
||||||
end,
|
|
||||||
add_acl(ParamsN, [{Login, format_msg(Re)} | ReList]);
|
|
||||||
|
|
||||||
add_acl([], ReList) ->
|
|
||||||
{ok, ReList}.
|
{ok, ReList}.
|
||||||
|
|
||||||
delete(#{login := Login, topic := Topic}, _) ->
|
do_add(Params) ->
|
||||||
return(emqx_auth_mnesia_cli:remove_acl(urldecode(Login), urldecode(Topic))).
|
Clientid = get_value(<<"clientid">>, Params, undefined),
|
||||||
|
Username = get_value(<<"username">>, Params, undefined),
|
||||||
|
Login = case {Clientid, Username} of
|
||||||
|
{undefined, undefined} -> all;
|
||||||
|
{_, undefined} -> {clientid, urldecode(Clientid)};
|
||||||
|
{undefined, _} -> {username, urldecode(Username)}
|
||||||
|
end,
|
||||||
|
Topic = urldecode(get_value(<<"topic">>, Params)),
|
||||||
|
Action = urldecode(get_value(<<"action">>, Params)),
|
||||||
|
Access = urldecode(get_value(<<"access">>, Params)),
|
||||||
|
Re = case validate([login, topic, action, access], [Login, Topic, Action, Access]) of
|
||||||
|
ok ->
|
||||||
|
emqx_acl_mnesia_cli:add_acl(Login, Topic, erlang:binary_to_atom(Action, utf8), erlang:binary_to_atom(Access, utf8));
|
||||||
|
Err -> Err
|
||||||
|
end,
|
||||||
|
maps:merge(#{topic => Topic,
|
||||||
|
action => Action,
|
||||||
|
access => Access,
|
||||||
|
result => format_msg(Re)
|
||||||
|
}, case Login of
|
||||||
|
all -> #{all => '$all'};
|
||||||
|
_ -> maps:from_list([Login])
|
||||||
|
end).
|
||||||
|
|
||||||
|
delete(#{clientid := Clientid, topic := Topic}, _) ->
|
||||||
|
return(emqx_acl_mnesia_cli:remove_acl({clientid, urldecode(Clientid)}, urldecode(Topic)));
|
||||||
|
delete(#{username := Username, topic := Topic}, _) ->
|
||||||
|
return(emqx_acl_mnesia_cli:remove_acl({username, urldecode(Username)}, urldecode(Topic)));
|
||||||
|
delete(#{topic := Topic}, _) ->
|
||||||
|
return(emqx_acl_mnesia_cli:remove_acl(all, urldecode(Topic))).
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
%% Interval Funcs
|
%% Interval Funcs
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
format({{clientid, Clientid}, Topic, Action, Access, _CreatedAt}) ->
|
||||||
format(#emqx_acl{login = Login, topic = Topic, action = Action, allow = Allow}) ->
|
#{clientid => Clientid, topic => Topic, action => Action, access => Access};
|
||||||
#{login => Login, topic => Topic, action => Action, allow => Allow };
|
format({{username, Username}, Topic, Action, Access, _CreatedAt}) ->
|
||||||
|
#{username => Username, topic => Topic, action => Action, access => Access};
|
||||||
format([]) ->
|
format({all, Topic, Action, Access, _CreatedAt}) ->
|
||||||
#{};
|
#{all => '$all', topic => Topic, action => Action, access => Access};
|
||||||
|
format(List) when is_list(List) ->
|
||||||
format([#emqx_acl{login = Login, topic = Topic, action = Action, allow = Allow}]) ->
|
|
||||||
format(#emqx_acl{login = Login, topic = Topic, action = Action, allow = Allow});
|
|
||||||
|
|
||||||
format([ #emqx_acl{login = _Key, topic = _Topic, action = _Action, allow = _Allow}| _] = List) ->
|
|
||||||
format(List, []).
|
format(List, []).
|
||||||
|
|
||||||
format([#emqx_acl{login = Login, topic = Topic, action = Action, allow = Allow} | List], ReList) ->
|
format([L | List], Relist) ->
|
||||||
format(List, [ format(#emqx_acl{login = Login, topic = Topic, action = Action, allow = Allow}) | ReList]);
|
format(List, [format(L) | Relist]);
|
||||||
format([], ReList) -> ReList.
|
format([], ReList) -> lists:reverse(ReList).
|
||||||
|
|
||||||
validate([], []) ->
|
validate([], []) ->
|
||||||
ok;
|
ok;
|
||||||
|
@ -114,8 +194,18 @@ validate([K|Keys], [V|Values]) ->
|
||||||
false -> {error, K};
|
false -> {error, K};
|
||||||
true -> validate(Keys, Values)
|
true -> validate(Keys, Values)
|
||||||
end.
|
end.
|
||||||
|
do_validation(login, all) ->
|
||||||
do_validation(login, V) when is_binary(V)
|
true;
|
||||||
|
do_validation(login, {clientid, V}) when is_binary(V)
|
||||||
|
andalso byte_size(V) > 0->
|
||||||
|
true;
|
||||||
|
do_validation(login, {username, V}) when is_binary(V)
|
||||||
|
andalso byte_size(V) > 0->
|
||||||
|
true;
|
||||||
|
do_validation(clientid, V) when is_binary(V)
|
||||||
|
andalso byte_size(V) > 0 ->
|
||||||
|
true;
|
||||||
|
do_validation(username, V) when is_binary(V)
|
||||||
andalso byte_size(V) > 0 ->
|
andalso byte_size(V) > 0 ->
|
||||||
true;
|
true;
|
||||||
do_validation(topic, V) when is_binary(V)
|
do_validation(topic, V) when is_binary(V)
|
||||||
|
@ -126,7 +216,7 @@ do_validation(action, V) when is_binary(V) ->
|
||||||
true -> true;
|
true -> true;
|
||||||
false -> false
|
false -> false
|
||||||
end;
|
end;
|
||||||
do_validation(allow, V) when is_boolean(V) ->
|
do_validation(access, V) when V =:= <<"allow">> orelse V =:= <<"deny">> ->
|
||||||
true;
|
true;
|
||||||
do_validation(_, _) ->
|
do_validation(_, _) ->
|
||||||
false.
|
false.
|
||||||
|
@ -145,4 +235,3 @@ urldecode(S) ->
|
||||||
urldecode(S) ->
|
urldecode(S) ->
|
||||||
http_uri:decode(S).
|
http_uri:decode(S).
|
||||||
-endif.
|
-endif.
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,198 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2020 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_acl_mnesia_cli).
|
||||||
|
|
||||||
|
-include("emqx_auth_mnesia.hrl").
|
||||||
|
-include_lib("emqx_libs/include/logger.hrl").
|
||||||
|
-include_lib("stdlib/include/ms_transform.hrl").
|
||||||
|
-define(TABLE, emqx_acl).
|
||||||
|
|
||||||
|
%% Acl APIs
|
||||||
|
-export([ add_acl/4
|
||||||
|
, lookup_acl/1
|
||||||
|
, all_acls/0
|
||||||
|
, all_acls/1
|
||||||
|
, remove_acl/2
|
||||||
|
]).
|
||||||
|
|
||||||
|
-export([cli/1]).
|
||||||
|
-export([comparing/2]).
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Acl API
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
%% @doc Add Acls
|
||||||
|
-spec(add_acl(login() |all, emqx_topic:topic(), pub | sub| pubsub, allow | deny) -> ok | {error, any()}).
|
||||||
|
add_acl(Login, Topic, Action, Access) ->
|
||||||
|
Acls = #?TABLE{
|
||||||
|
filter = {Login, Topic},
|
||||||
|
action = Action,
|
||||||
|
access = Access,
|
||||||
|
created_at = erlang:system_time(millisecond)
|
||||||
|
},
|
||||||
|
ret(mnesia:transaction(fun mnesia:write/1, [Acls])).
|
||||||
|
|
||||||
|
%% @doc Lookup acl by login
|
||||||
|
-spec(lookup_acl(login() | all) -> list()).
|
||||||
|
lookup_acl(undefined) -> [];
|
||||||
|
lookup_acl(Login) ->
|
||||||
|
MatchSpec = ets:fun2ms(fun({?TABLE, {Filter, ACLTopic}, Action, Access, CreatedAt})
|
||||||
|
when Filter =:= Login -> {Filter, ACLTopic, Action, Access, CreatedAt} end),
|
||||||
|
lists:sort(fun comparing/2, ets:select(?TABLE, MatchSpec)).
|
||||||
|
|
||||||
|
%% @doc Remove acl
|
||||||
|
-spec(remove_acl(login() | all, emqx_topic:topic()) -> ok | {error, any()}).
|
||||||
|
remove_acl(Login, Topic) ->
|
||||||
|
ret(mnesia:transaction(fun mnesia:delete/1, [{?TABLE, {Login, Topic}}])).
|
||||||
|
|
||||||
|
%% @doc All logins
|
||||||
|
-spec(all_acls() -> list()).
|
||||||
|
all_acls() ->
|
||||||
|
all_acls(clientid) ++
|
||||||
|
all_acls(username) ++
|
||||||
|
all_acls(all).
|
||||||
|
|
||||||
|
all_acls(clientid) ->
|
||||||
|
MatchSpec = ets:fun2ms(fun({?TABLE, {{clientid, Clientid}, Topic}, Action, Access, CreatedAt}) -> {{clientid, Clientid}, Topic, Action, Access, CreatedAt} end),
|
||||||
|
lists:sort(fun comparing/2, ets:select(?TABLE, MatchSpec));
|
||||||
|
all_acls(username) ->
|
||||||
|
MatchSpec = ets:fun2ms(fun({?TABLE, {{username, Username}, Topic}, Action, Access, CreatedAt}) -> {{username, Username}, Topic, Action, Access, CreatedAt} end),
|
||||||
|
lists:sort(fun comparing/2, ets:select(?TABLE, MatchSpec));
|
||||||
|
all_acls(all) ->
|
||||||
|
MatchSpec = ets:fun2ms(fun({?TABLE, {all, Topic}, Action, Access, CreatedAt}) -> {all, Topic, Action, Access, CreatedAt} end),
|
||||||
|
lists:sort(fun comparing/2, ets:select(?TABLE, MatchSpec)).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Internal functions
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
comparing({_, _, _, _, CreatedAt1},
|
||||||
|
{_, _, _, _, CreatedAt2}) ->
|
||||||
|
CreatedAt1 >= CreatedAt2.
|
||||||
|
|
||||||
|
ret({atomic, ok}) -> ok;
|
||||||
|
ret({aborted, Error}) -> {error, Error}.
|
||||||
|
|
||||||
|
validate(action, "pub") -> true;
|
||||||
|
validate(action, "sub") -> true;
|
||||||
|
validate(action, "pubsub") -> true;
|
||||||
|
validate(access, "allow") -> true;
|
||||||
|
validate(access, "deny") -> true;
|
||||||
|
validate(_, _) -> false.
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% ACL Cli
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
cli(["list"]) ->
|
||||||
|
[ begin
|
||||||
|
case Filter of
|
||||||
|
{clientid, Clientid} ->
|
||||||
|
emqx_ctl:print("Acl(clientid = ~p topic = ~p action = ~p access = ~p)~n",[Clientid, Topic, Action, Access]);
|
||||||
|
{username, Username} ->
|
||||||
|
emqx_ctl:print("Acl(username = ~p topic = ~p action = ~p access = ~p)~n",[Username, Topic, Action, Access]);
|
||||||
|
all ->
|
||||||
|
emqx_ctl:print("Acl($all topic = ~p action = ~p access = ~p)~n",[Topic, Action, Access])
|
||||||
|
end
|
||||||
|
end || {Filter, Topic, Action, Access, _} <- all_acls()];
|
||||||
|
|
||||||
|
cli(["list", "clientid"]) ->
|
||||||
|
[emqx_ctl:print("Acl(clientid = ~p topic = ~p action = ~p access = ~p)~n",[Clientid, Topic, Action, Access])
|
||||||
|
|| {{clientid, Clientid}, Topic, Action, Access, _} <- all_acls(clientid) ];
|
||||||
|
|
||||||
|
cli(["list", "username"]) ->
|
||||||
|
[emqx_ctl:print("Acl(username = ~p topic = ~p action = ~p access = ~p)~n",[Username, Topic, Action, Access])
|
||||||
|
|| {{username, Username}, Topic, Action, Access, _} <- all_acls(username) ];
|
||||||
|
|
||||||
|
cli(["list", "_all"]) ->
|
||||||
|
[emqx_ctl:print("Acl($all topic = ~p action = ~p access = ~p)~n",[Topic, Action, Access])
|
||||||
|
|| {all, Topic, Action, Access, _} <- all_acls(all) ];
|
||||||
|
|
||||||
|
cli(["add", "clientid", Clientid, Topic, Action, Access]) ->
|
||||||
|
case validate(action, Action) andalso validate(access, Access) of
|
||||||
|
true ->
|
||||||
|
case add_acl({clientid, iolist_to_binary(Clientid)}, iolist_to_binary(Topic), list_to_existing_atom(Action), list_to_existing_atom(Access)) of
|
||||||
|
ok -> emqx_ctl:print("ok~n");
|
||||||
|
{error, Reason} -> emqx_ctl:print("Error: ~p~n", [Reason])
|
||||||
|
end;
|
||||||
|
_ ->
|
||||||
|
emqx_ctl:print("Error: Input is illegal~n")
|
||||||
|
end;
|
||||||
|
|
||||||
|
cli(["add", "username", Username, Topic, Action, Access]) ->
|
||||||
|
case validate(action, Action) andalso validate(access, Access) of
|
||||||
|
true ->
|
||||||
|
case add_acl({username, iolist_to_binary(Username)}, iolist_to_binary(Topic), list_to_existing_atom(Action), list_to_existing_atom(Access)) of
|
||||||
|
ok -> emqx_ctl:print("ok~n");
|
||||||
|
{error, Reason} -> emqx_ctl:print("Error: ~p~n", [Reason])
|
||||||
|
end;
|
||||||
|
_ ->
|
||||||
|
emqx_ctl:print("Error: Input is illegal~n")
|
||||||
|
end;
|
||||||
|
|
||||||
|
cli(["add", "_all", Topic, Action, Access]) ->
|
||||||
|
case validate(action, Action) andalso validate(access, Access) of
|
||||||
|
true ->
|
||||||
|
case add_acl(all, iolist_to_binary(Topic), list_to_existing_atom(Action), list_to_existing_atom(Access)) of
|
||||||
|
ok -> emqx_ctl:print("ok~n");
|
||||||
|
{error, Reason} -> emqx_ctl:print("Error: ~p~n", [Reason])
|
||||||
|
end;
|
||||||
|
_ ->
|
||||||
|
emqx_ctl:print("Error: Input is illegal~n")
|
||||||
|
end;
|
||||||
|
|
||||||
|
cli(["show", "clientid", Clientid]) ->
|
||||||
|
[emqx_ctl:print("Acl(clientid = ~p topic = ~p action = ~p access = ~p)~n",[NClientid, Topic, Action, Access])
|
||||||
|
|| {{clientid, NClientid}, Topic, Action, Access, _} <- lookup_acl({clientid, iolist_to_binary(Clientid)}) ];
|
||||||
|
|
||||||
|
cli(["show", "username", Username]) ->
|
||||||
|
[emqx_ctl:print("Acl(username = ~p topic = ~p action = ~p access = ~p)~n",[NUsername, Topic, Action, Access])
|
||||||
|
|| {{username, NUsername}, Topic, Action, Access, _} <- lookup_acl({username, iolist_to_binary(Username)}) ];
|
||||||
|
|
||||||
|
cli(["del", "clientid", Clientid, Topic])->
|
||||||
|
case remove_acl({clientid, iolist_to_binary(Clientid)}, iolist_to_binary(Topic)) of
|
||||||
|
ok -> emqx_ctl:print("ok~n");
|
||||||
|
{error, Reason} -> emqx_ctl:print("Error: ~p~n", [Reason])
|
||||||
|
end;
|
||||||
|
|
||||||
|
cli(["del", "username", Username, Topic])->
|
||||||
|
case remove_acl({username, iolist_to_binary(Username)}, iolist_to_binary(Topic)) of
|
||||||
|
ok -> emqx_ctl:print("ok~n");
|
||||||
|
{error, Reason} -> emqx_ctl:print("Error: ~p~n", [Reason])
|
||||||
|
end;
|
||||||
|
|
||||||
|
cli(["del", "_all", Topic])->
|
||||||
|
case remove_acl(all, iolist_to_binary(Topic)) of
|
||||||
|
ok -> emqx_ctl:print("ok~n");
|
||||||
|
{error, Reason} -> emqx_ctl:print("Error: ~p~n", [Reason])
|
||||||
|
end;
|
||||||
|
|
||||||
|
cli(_) ->
|
||||||
|
emqx_ctl:usage([ {"acl list clientid","List clientid acls"}
|
||||||
|
, {"acl list username","List username acls"}
|
||||||
|
, {"acl list _all","List $all acls"}
|
||||||
|
, {"acl show clientid <Clientid>", "Lookup clientid acl detail"}
|
||||||
|
, {"acl show username <Username>", "Lookup username acl detail"}
|
||||||
|
, {"acl aad clientid <Clientid> <Topic> <Action> <Access>", "Add clientid acl"}
|
||||||
|
, {"acl add Username <Username> <Topic> <Action> <Access>", "Add username acl"}
|
||||||
|
, {"acl add _all <Topic> <Action> <Access>", "Add $all acl"}
|
||||||
|
, {"acl del clientid <Clientid> <Topic>", "Delete clientid acl"}
|
||||||
|
, {"acl del username <Username> <Topic>", "Delete username acl"}
|
||||||
|
, {"acl del _all, <Topic>", "Delete $all acl"}
|
||||||
|
]).
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
{application, emqx_auth_mnesia,
|
{application, emqx_auth_mnesia,
|
||||||
[{description, "EMQ X Authentication with Mnesia"},
|
[{description, "EMQ X Authentication with Mnesia"},
|
||||||
{vsn, "5.0.0"}, % strict semver, bump manually!
|
{vsn, "4.3.0"}, % strict semver, bump manually
|
||||||
{modules, []},
|
{modules, []},
|
||||||
{registered, []},
|
{registered, []},
|
||||||
{applications, [kernel,stdlib,mnesia,emqx_libs]},
|
{applications, [kernel,stdlib,mnesia]},
|
||||||
{mod, {emqx_auth_mnesia_app,[]}},
|
{mod, {emqx_auth_mnesia_app,[]}},
|
||||||
{env, []},
|
{env, []},
|
||||||
{licenses, ["Apache-2.0"]},
|
{licenses, ["Apache-2.0"]},
|
||||||
|
|
|
@ -22,6 +22,9 @@
|
||||||
-include_lib("emqx_libs/include/logger.hrl").
|
-include_lib("emqx_libs/include/logger.hrl").
|
||||||
-include_lib("emqx_libs/include/types.hrl").
|
-include_lib("emqx_libs/include/types.hrl").
|
||||||
|
|
||||||
|
-include_lib("stdlib/include/ms_transform.hrl").
|
||||||
|
|
||||||
|
-define(TABLE, emqx_user).
|
||||||
%% Auth callbacks
|
%% Auth callbacks
|
||||||
-export([ init/1
|
-export([ init/1
|
||||||
, register_metrics/0
|
, register_metrics/0
|
||||||
|
@ -29,48 +32,53 @@
|
||||||
, description/0
|
, description/0
|
||||||
]).
|
]).
|
||||||
|
|
||||||
init(DefaultUsers) ->
|
init(#{clientid_list := ClientidList, username_list := UsernameList}) ->
|
||||||
ok = ekka_mnesia:create_table(emqx_user, [
|
ok = ekka_mnesia:create_table(emqx_user, [
|
||||||
{disc_copies, [node()]},
|
{disc_copies, [node()]},
|
||||||
{attributes, record_info(fields, emqx_user)},
|
{attributes, record_info(fields, emqx_user)},
|
||||||
{storage_properties, [{ets, [{read_concurrency, true}]}]}]),
|
{storage_properties, [{ets, [{read_concurrency, true}]}]}]),
|
||||||
ok = lists:foreach(fun add_default_user/1, DefaultUsers),
|
[ add_default_user({{clientid, iolist_to_binary(Clientid)}, iolist_to_binary(Password)})
|
||||||
|
|| {Clientid, Password} <- ClientidList],
|
||||||
|
[ add_default_user({{username, iolist_to_binary(Username)}, iolist_to_binary(Password)})
|
||||||
|
|| {Username, Password} <- UsernameList],
|
||||||
ok = ekka_mnesia:copy_table(emqx_user, disc_copies).
|
ok = ekka_mnesia:copy_table(emqx_user, disc_copies).
|
||||||
|
|
||||||
%% @private
|
%% @private
|
||||||
add_default_user({Login, Password, IsSuperuser}) ->
|
add_default_user({Login, Password}) when is_tuple(Login) ->
|
||||||
emqx_auth_mnesia_cli:add_user(iolist_to_binary(Login), iolist_to_binary(Password), IsSuperuser).
|
emqx_auth_mnesia_cli:add_user(Login, Password).
|
||||||
|
|
||||||
-spec(register_metrics() -> ok).
|
-spec(register_metrics() -> ok).
|
||||||
register_metrics() ->
|
register_metrics() ->
|
||||||
lists:foreach(fun emqx_metrics:ensure/1, ?AUTH_METRICS).
|
lists:foreach(fun emqx_metrics:ensure/1, ?AUTH_METRICS).
|
||||||
|
|
||||||
check(ClientInfo = #{password := Password}, AuthResult, #{hash_type := HashType, key_as := As}) ->
|
check(ClientInfo = #{ clientid := Clientid
|
||||||
Login = maps:get(As, ClientInfo),
|
, password := NPassword
|
||||||
case emqx_auth_mnesia_cli:lookup_user(Login) of
|
}, AuthResult, #{hash_type := HashType}) ->
|
||||||
|
Username = maps:get(username, ClientInfo, undefined),
|
||||||
|
MatchSpec = ets:fun2ms(fun({?TABLE, {clientid, X }, Password, InterTime}) when X =:= Clientid-> Password;
|
||||||
|
({?TABLE, {username, X }, Password, InterTime}) when X =:= Username andalso X =/= undefined -> Password
|
||||||
|
end),
|
||||||
|
case ets:select(?TABLE, MatchSpec) of
|
||||||
[] ->
|
[] ->
|
||||||
emqx_metrics:inc(?AUTH_METRICS(ignore)),
|
emqx_metrics:inc(?AUTH_METRICS(ignore)),
|
||||||
ok;
|
ok;
|
||||||
[User] ->
|
List ->
|
||||||
case emqx_passwd:check_pass({User#emqx_user.password, Password}, HashType) of
|
case [ Hash || <<Salt:4/binary, Hash/binary>> <- lists:sort(fun emqx_auth_mnesia_cli:comparing/2, List),
|
||||||
ok ->
|
Hash =:= hash(NPassword, Salt, HashType)
|
||||||
emqx_metrics:inc(?AUTH_METRICS(success)),
|
] of
|
||||||
{stop, AuthResult#{is_superuser => is_superuser(User),
|
[] ->
|
||||||
anonymous => false,
|
?LOG(error, "[Mnesia] Auth from mnesia failed: ~p", [ClientInfo]),
|
||||||
auth_result => success}};
|
|
||||||
{error, Reason} ->
|
|
||||||
?LOG(error, "[Mnesia] Auth from mnesia failed: ~p", [Reason]),
|
|
||||||
emqx_metrics:inc(?AUTH_METRICS(failure)),
|
emqx_metrics:inc(?AUTH_METRICS(failure)),
|
||||||
{stop, AuthResult#{auth_result => password_error, anonymous => false}}
|
{stop, AuthResult#{anonymous => false, auth_result => password_error}};
|
||||||
|
_ ->
|
||||||
|
emqx_metrics:inc(?AUTH_METRICS(success)),
|
||||||
|
{stop, AuthResult#{anonymous => false, auth_result => success}}
|
||||||
end
|
end
|
||||||
end.
|
end.
|
||||||
|
|
||||||
description() -> "Authentication with Mnesia".
|
description() -> "Authentication with Mnesia".
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
hash(undefined, SaltBin, HashType) ->
|
||||||
%% Internal functions
|
hash(<<>>, SaltBin, HashType);
|
||||||
%%--------------------------------------------------------------------
|
hash(Password, SaltBin, HashType) ->
|
||||||
is_superuser(#emqx_user{is_superuser = true}) ->
|
emqx_passwd:hash(HashType, <<SaltBin/binary, Password/binary>>).
|
||||||
true;
|
|
||||||
is_superuser(_) ->
|
|
||||||
false.
|
|
||||||
|
|
|
@ -17,100 +17,206 @@
|
||||||
-module(emqx_auth_mnesia_api).
|
-module(emqx_auth_mnesia_api).
|
||||||
|
|
||||||
-include_lib("stdlib/include/qlc.hrl").
|
-include_lib("stdlib/include/qlc.hrl").
|
||||||
|
-include_lib("stdlib/include/ms_transform.hrl").
|
||||||
|
|
||||||
|
-define(TABLE, emqx_user).
|
||||||
|
|
||||||
-import(proplists, [get_value/2]).
|
-import(proplists, [get_value/2]).
|
||||||
|
|
||||||
-import(minirest, [return/1]).
|
-import(minirest, [return/1]).
|
||||||
|
-export([paginate/5]).
|
||||||
|
|
||||||
-rest_api(#{name => list_emqx_user,
|
-export([ list_clientid/2
|
||||||
method => 'GET',
|
, lookup_clientid/2
|
||||||
path => "/mqtt_user",
|
, add_clientid/2
|
||||||
func => list,
|
, update_clientid/2
|
||||||
descr => "List available mnesia in the cluster"
|
, delete_clientid/2
|
||||||
}).
|
|
||||||
|
|
||||||
-rest_api(#{name => lookup_emqx_user,
|
|
||||||
method => 'GET',
|
|
||||||
path => "/mqtt_user/:bin:login",
|
|
||||||
func => lookup,
|
|
||||||
descr => "Lookup mnesia in the cluster"
|
|
||||||
}).
|
|
||||||
|
|
||||||
-rest_api(#{name => add_emqx_user,
|
|
||||||
method => 'POST',
|
|
||||||
path => "/mqtt_user",
|
|
||||||
func => add,
|
|
||||||
descr => "Add mnesia in the cluster"
|
|
||||||
}).
|
|
||||||
|
|
||||||
-rest_api(#{name => update_emqx_user,
|
|
||||||
method => 'PUT',
|
|
||||||
path => "/mqtt_user/:bin:login",
|
|
||||||
func => update,
|
|
||||||
descr => "Update mnesia in the cluster"
|
|
||||||
}).
|
|
||||||
|
|
||||||
-rest_api(#{name => delete_emqx_user,
|
|
||||||
method => 'DELETE',
|
|
||||||
path => "/mqtt_user/:bin:login",
|
|
||||||
func => delete,
|
|
||||||
descr => "Delete mnesia in the cluster"
|
|
||||||
}).
|
|
||||||
|
|
||||||
-export([ list/2
|
|
||||||
, lookup/2
|
|
||||||
, add/2
|
|
||||||
, update/2
|
|
||||||
, delete/2
|
|
||||||
]).
|
]).
|
||||||
|
|
||||||
-export([paginate/3]).
|
-rest_api(#{name => list_clientid,
|
||||||
|
method => 'GET',
|
||||||
|
path => "/auth_clientid",
|
||||||
|
func => list_clientid,
|
||||||
|
descr => "List available clientid in the cluster"
|
||||||
|
}).
|
||||||
|
|
||||||
list(_Bindings, Params) ->
|
-rest_api(#{name => lookup_clientid,
|
||||||
return({ok, paginate(emqx_user, Params, fun format/1)}).
|
method => 'GET',
|
||||||
|
path => "/auth_clientid/:bin:clientid",
|
||||||
|
func => lookup_clientid,
|
||||||
|
descr => "Lookup clientid in the cluster"
|
||||||
|
}).
|
||||||
|
|
||||||
lookup(#{login := Login}, _Params) ->
|
-rest_api(#{name => add_clientid,
|
||||||
return({ok, format(emqx_auth_mnesia_cli:lookup_user(urldecode(Login)))}).
|
method => 'POST',
|
||||||
|
path => "/auth_clientid",
|
||||||
|
func => add_clientid,
|
||||||
|
descr => "Add clientid in the cluster"
|
||||||
|
}).
|
||||||
|
|
||||||
add(_Bindings, Params) ->
|
-rest_api(#{name => update_clientid,
|
||||||
|
method => 'PUT',
|
||||||
|
path => "/auth_clientid/:bin:clientid",
|
||||||
|
func => update_clientid,
|
||||||
|
descr => "Update clientid in the cluster"
|
||||||
|
}).
|
||||||
|
|
||||||
|
-rest_api(#{name => delete_clientid,
|
||||||
|
method => 'DELETE',
|
||||||
|
path => "/auth_clientid/:bin:clientid",
|
||||||
|
func => delete_clientid,
|
||||||
|
descr => "Delete clientid in the cluster"
|
||||||
|
}).
|
||||||
|
|
||||||
|
-export([ list_username/2
|
||||||
|
, lookup_username/2
|
||||||
|
, add_username/2
|
||||||
|
, update_username/2
|
||||||
|
, delete_username/2
|
||||||
|
]).
|
||||||
|
|
||||||
|
-rest_api(#{name => list_username,
|
||||||
|
method => 'GET',
|
||||||
|
path => "/auth_username",
|
||||||
|
func => list_username,
|
||||||
|
descr => "List available username in the cluster"
|
||||||
|
}).
|
||||||
|
|
||||||
|
-rest_api(#{name => lookup_username,
|
||||||
|
method => 'GET',
|
||||||
|
path => "/auth_username/:bin:username",
|
||||||
|
func => lookup_username,
|
||||||
|
descr => "Lookup username in the cluster"
|
||||||
|
}).
|
||||||
|
|
||||||
|
-rest_api(#{name => add_username,
|
||||||
|
method => 'POST',
|
||||||
|
path => "/auth_username",
|
||||||
|
func => add_username,
|
||||||
|
descr => "Add username in the cluster"
|
||||||
|
}).
|
||||||
|
|
||||||
|
-rest_api(#{name => update_username,
|
||||||
|
method => 'PUT',
|
||||||
|
path => "/auth_username/:bin:username",
|
||||||
|
func => update_username,
|
||||||
|
descr => "Update username in the cluster"
|
||||||
|
}).
|
||||||
|
|
||||||
|
-rest_api(#{name => delete_username,
|
||||||
|
method => 'DELETE',
|
||||||
|
path => "/auth_username/:bin:username",
|
||||||
|
func => delete_username,
|
||||||
|
descr => "Delete username in the cluster"
|
||||||
|
}).
|
||||||
|
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
%% Auth Clientid Api
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
list_clientid(_Bindings, Params) ->
|
||||||
|
MatchSpec = ets:fun2ms(fun({?TABLE, {clientid, Clientid}, Password, CreatedAt}) -> {?TABLE, {clientid, Clientid}, Password, CreatedAt} end),
|
||||||
|
return({ok, paginate(?TABLE, MatchSpec, Params, fun emqx_auth_mnesia_cli:comparing/2, fun({?TABLE, {clientid, X}, _, _}) -> #{clientid => X} end)}).
|
||||||
|
|
||||||
|
lookup_clientid(#{clientid := Clientid}, _Params) ->
|
||||||
|
return({ok, format(emqx_auth_mnesia_cli:lookup_user({clientid, urldecode(Clientid)}))}).
|
||||||
|
|
||||||
|
add_clientid(_Bindings, Params) ->
|
||||||
[ P | _] = Params,
|
[ P | _] = Params,
|
||||||
case is_list(P) of
|
case is_list(P) of
|
||||||
true -> return(add_user(Params, []));
|
true -> return(do_add_clientid(Params, []));
|
||||||
false -> return(add_user([Params], []))
|
false ->
|
||||||
|
Re = do_add_clientid(Params),
|
||||||
|
case Re of
|
||||||
|
ok -> return(ok);
|
||||||
|
<<"ok">> -> return(ok);
|
||||||
|
_ -> return({error, format_msg(Re)})
|
||||||
|
end
|
||||||
end.
|
end.
|
||||||
|
|
||||||
add_user([ Params | ParamsN ], ReList ) ->
|
do_add_clientid([ Params | ParamsN ], ReList ) ->
|
||||||
Login = urldecode(get_value(<<"login">>, Params)),
|
Clientid = urldecode(get_value(<<"clientid">>, Params)),
|
||||||
Password = urldecode(get_value(<<"password">>, Params)),
|
do_add_clientid(ParamsN, [{Clientid, format_msg(do_add_clientid(Params))} | ReList]);
|
||||||
IsSuperuser = get_value(<<"is_superuser">>, Params),
|
|
||||||
Re = case validate([login, password, is_superuser], [Login, Password, IsSuperuser]) of
|
do_add_clientid([], ReList) ->
|
||||||
ok ->
|
|
||||||
emqx_auth_mnesia_cli:add_user(Login, Password, IsSuperuser);
|
|
||||||
Err -> Err
|
|
||||||
end,
|
|
||||||
add_user(ParamsN, [{Login, format_msg(Re)} | ReList]);
|
|
||||||
|
|
||||||
add_user([], ReList) ->
|
|
||||||
{ok, ReList}.
|
{ok, ReList}.
|
||||||
|
|
||||||
update(#{login := Login}, Params) ->
|
do_add_clientid(Params) ->
|
||||||
|
Clientid = urldecode(get_value(<<"clientid">>, Params)),
|
||||||
|
Password = urldecode(get_value(<<"password">>, Params)),
|
||||||
|
Login = {clientid, Clientid},
|
||||||
|
case validate([login, password], [Login, Password]) of
|
||||||
|
ok ->
|
||||||
|
emqx_auth_mnesia_cli:add_user(Login, Password);
|
||||||
|
Err -> Err
|
||||||
|
end.
|
||||||
|
|
||||||
|
update_clientid(#{clientid := Clientid}, Params) ->
|
||||||
Password = get_value(<<"password">>, Params),
|
Password = get_value(<<"password">>, Params),
|
||||||
IsSuperuser = get_value(<<"is_superuser">>, Params),
|
case validate([password], [Password]) of
|
||||||
case validate([password, is_superuser], [Password, IsSuperuser]) of
|
ok -> return(emqx_auth_mnesia_cli:update_user({clientid, urldecode(Clientid)}, urldecode(Password)));
|
||||||
ok -> return(emqx_auth_mnesia_cli:update_user(urldecode(Login), urldecode(Password), IsSuperuser));
|
|
||||||
Err -> return(Err)
|
Err -> return(Err)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
delete(#{login := Login}, _) ->
|
delete_clientid(#{clientid := Clientid}, _) ->
|
||||||
return(emqx_auth_mnesia_cli:remove_user(urldecode(Login))).
|
return(emqx_auth_mnesia_cli:remove_user({clientid, urldecode(Clientid)})).
|
||||||
|
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
%% Auth Username Api
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
list_username(_Bindings, Params) ->
|
||||||
|
MatchSpec = ets:fun2ms(fun({?TABLE, {username, Username}, Password, CreatedAt}) -> {?TABLE, {username, Username}, Password, CreatedAt} end),
|
||||||
|
return({ok, paginate(?TABLE, MatchSpec, Params, fun emqx_auth_mnesia_cli:comparing/2, fun({?TABLE, {username, X}, _, _}) -> #{username => X} end)}).
|
||||||
|
|
||||||
|
lookup_username(#{username := Username}, _Params) ->
|
||||||
|
return({ok, format(emqx_auth_mnesia_cli:lookup_user({username, urldecode(Username)}))}).
|
||||||
|
|
||||||
|
add_username(_Bindings, Params) ->
|
||||||
|
[ P | _] = Params,
|
||||||
|
case is_list(P) of
|
||||||
|
true -> return(do_add_username(Params, []));
|
||||||
|
false ->
|
||||||
|
case do_add_username(Params) of
|
||||||
|
ok -> return(ok);
|
||||||
|
<<"ok">> -> return(ok);
|
||||||
|
Error -> return({error, format_msg(Error)})
|
||||||
|
end
|
||||||
|
end.
|
||||||
|
|
||||||
|
do_add_username([ Params | ParamsN ], ReList ) ->
|
||||||
|
Username = urldecode(get_value(<<"username">>, Params)),
|
||||||
|
do_add_username(ParamsN, [{Username, format_msg(do_add_username(Params))} | ReList]);
|
||||||
|
|
||||||
|
do_add_username([], ReList) ->
|
||||||
|
{ok, ReList}.
|
||||||
|
|
||||||
|
do_add_username(Params) ->
|
||||||
|
Username = urldecode(get_value(<<"username">>, Params)),
|
||||||
|
Password = urldecode(get_value(<<"password">>, Params)),
|
||||||
|
Login = {username, Username},
|
||||||
|
case validate([login, password], [Login, Password]) of
|
||||||
|
ok ->
|
||||||
|
emqx_auth_mnesia_cli:add_user(Login, Password);
|
||||||
|
Err -> Err
|
||||||
|
end.
|
||||||
|
|
||||||
|
update_username(#{username := Username}, Params) ->
|
||||||
|
Password = get_value(<<"password">>, Params),
|
||||||
|
case validate([password], [Password]) of
|
||||||
|
ok -> return(emqx_auth_mnesia_cli:update_user({username, urldecode(Username)}, urldecode(Password)));
|
||||||
|
Err -> return(Err)
|
||||||
|
end.
|
||||||
|
|
||||||
|
delete_username(#{username := Username}, _) ->
|
||||||
|
return(emqx_auth_mnesia_cli:remove_user({username, urldecode(Username)})).
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
%% Paging Query
|
%% Paging Query
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
paginate(Tables, Params, RowFun) ->
|
paginate(Tables, MatchSpec, Params, ComparingFun, RowFun) ->
|
||||||
Qh = query_handle(Tables),
|
Qh = query_handle(Tables, MatchSpec),
|
||||||
Count = count(Tables),
|
Count = count(Tables, MatchSpec),
|
||||||
Page = page(Params),
|
Page = page(Params),
|
||||||
Limit = limit(Params),
|
Limit = limit(Params),
|
||||||
Cursor = qlc:cursor(Qh),
|
Cursor = qlc:cursor(Qh),
|
||||||
|
@ -121,21 +227,28 @@ paginate(Tables, Params, RowFun) ->
|
||||||
Rows = qlc:next_answers(Cursor, Limit),
|
Rows = qlc:next_answers(Cursor, Limit),
|
||||||
qlc:delete_cursor(Cursor),
|
qlc:delete_cursor(Cursor),
|
||||||
#{meta => #{page => Page, limit => Limit, count => Count},
|
#{meta => #{page => Page, limit => Limit, count => Count},
|
||||||
data => [RowFun(Row) || Row <- Rows]}.
|
data => [RowFun(Row) || Row <- lists:sort(ComparingFun, Rows)]}.
|
||||||
|
|
||||||
query_handle(Table) when is_atom(Table) ->
|
query_handle(Table, MatchSpec) when is_atom(Table) ->
|
||||||
qlc:q([R|| R <- ets:table(Table)]);
|
Options = {traverse, {select, MatchSpec}},
|
||||||
query_handle([Table]) when is_atom(Table) ->
|
qlc:q([R|| R <- ets:table(Table, Options)]);
|
||||||
qlc:q([R|| R <- ets:table(Table)]);
|
query_handle([Table], MatchSpec) when is_atom(Table) ->
|
||||||
query_handle(Tables) ->
|
Options = {traverse, {select, MatchSpec}},
|
||||||
qlc:append([qlc:q([E || E <- ets:table(T)]) || T <- Tables]).
|
qlc:q([R|| R <- ets:table(Table, Options)]);
|
||||||
|
query_handle(Tables, MatchSpec) ->
|
||||||
|
Options = {traverse, {select, MatchSpec}},
|
||||||
|
qlc:append([qlc:q([E || E <- ets:table(T, Options)]) || T <- Tables]).
|
||||||
|
|
||||||
count(Table) when is_atom(Table) ->
|
count(Table, MatchSpec) when is_atom(Table) ->
|
||||||
ets:info(Table, size);
|
[{MatchPattern, Where, _Re}] = MatchSpec,
|
||||||
count([Table]) when is_atom(Table) ->
|
NMatchSpec = [{MatchPattern, Where, [true]}],
|
||||||
ets:info(Table, size);
|
ets:select_count(Table, NMatchSpec);
|
||||||
count(Tables) ->
|
count([Table], MatchSpec) when is_atom(Table) ->
|
||||||
lists:sum([count(T) || T <- Tables]).
|
[{MatchPattern, Where, _Re}] = MatchSpec,
|
||||||
|
NMatchSpec = [{MatchPattern, Where, [true]}],
|
||||||
|
ets:select_count(Table, NMatchSpec);
|
||||||
|
count(Tables, MatchSpec) ->
|
||||||
|
lists:sum([count(T, MatchSpec) || T <- Tables]).
|
||||||
|
|
||||||
page(Params) ->
|
page(Params) ->
|
||||||
binary_to_integer(proplists:get_value(<<"_page">>, Params, <<"1">>)).
|
binary_to_integer(proplists:get_value(<<"_page">>, Params, <<"1">>)).
|
||||||
|
@ -146,24 +259,28 @@ limit(Params) ->
|
||||||
Size -> binary_to_integer(Size)
|
Size -> binary_to_integer(Size)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
%% Interval Funcs
|
%% Interval Funcs
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
format({emqx_user, Login, Password, IsSuperuser}) ->
|
format({?TABLE, {clientid, ClientId}, Password, _InterTime}) ->
|
||||||
#{login => Login,
|
#{clientid => ClientId,
|
||||||
password => Password,
|
password => Password};
|
||||||
is_superuser => IsSuperuser};
|
|
||||||
|
format({?TABLE, {username, Username}, Password, _InterTime}) ->
|
||||||
|
#{username => Username,
|
||||||
|
password => Password};
|
||||||
|
|
||||||
|
format([{?TABLE, {clientid, ClientId}, Password, _InterTime}]) ->
|
||||||
|
#{clientid => ClientId,
|
||||||
|
password => Password};
|
||||||
|
|
||||||
|
format([{?TABLE, {username, Username}, Password, _InterTime}]) ->
|
||||||
|
#{username => Username,
|
||||||
|
password => Password};
|
||||||
|
|
||||||
format([]) ->
|
format([]) ->
|
||||||
#{};
|
#{}.
|
||||||
|
|
||||||
format([{emqx_user, Login, Password, IsSuperuser}]) ->
|
|
||||||
#{login => Login,
|
|
||||||
password => Password,
|
|
||||||
is_superuser => IsSuperuser}.
|
|
||||||
|
|
||||||
validate([], []) ->
|
validate([], []) ->
|
||||||
ok;
|
ok;
|
||||||
|
@ -173,14 +290,15 @@ validate([K|Keys], [V|Values]) ->
|
||||||
true -> validate(Keys, Values)
|
true -> validate(Keys, Values)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
do_validation(login, V) when is_binary(V)
|
do_validation(login, {clientid, V}) when is_binary(V)
|
||||||
|
andalso byte_size(V) > 0 ->
|
||||||
|
true;
|
||||||
|
do_validation(login, {username, V}) when is_binary(V)
|
||||||
andalso byte_size(V) > 0 ->
|
andalso byte_size(V) > 0 ->
|
||||||
true;
|
true;
|
||||||
do_validation(password, V) when is_binary(V)
|
do_validation(password, V) when is_binary(V)
|
||||||
andalso byte_size(V) > 0 ->
|
andalso byte_size(V) > 0 ->
|
||||||
true;
|
true;
|
||||||
do_validation(is_superuser, V) when is_boolean(V) ->
|
|
||||||
true;
|
|
||||||
do_validation(_, _) ->
|
do_validation(_, _) ->
|
||||||
false.
|
false.
|
||||||
|
|
||||||
|
|
|
@ -34,8 +34,10 @@
|
||||||
|
|
||||||
start(_StartType, _StartArgs) ->
|
start(_StartType, _StartArgs) ->
|
||||||
{ok, Sup} = emqx_auth_mnesia_sup:start_link(),
|
{ok, Sup} = emqx_auth_mnesia_sup:start_link(),
|
||||||
emqx_ctl:register_command('mqtt-user', {emqx_auth_mnesia_cli, auth_cli}, []),
|
emqx_ctl:register_command(clientid, {emqx_auth_mnesia_cli, auth_clientid_cli}, []),
|
||||||
emqx_ctl:register_command('mqtt-acl', {emqx_auth_mnesia_cli, acl_cli}, []),
|
emqx_ctl:register_command(username, {emqx_auth_mnesia_cli, auth_username_cli}, []),
|
||||||
|
emqx_ctl:register_command(user, {emqx_auth_mnesia_cli, auth_username_cli}, []),
|
||||||
|
emqx_ctl:register_command(acl, {emqx_acl_mnesia_cli, cli}, []),
|
||||||
load_auth_hook(),
|
load_auth_hook(),
|
||||||
load_acl_hook(),
|
load_acl_hook(),
|
||||||
{ok, Sup}.
|
{ok, Sup}.
|
||||||
|
@ -43,28 +45,26 @@ start(_StartType, _StartArgs) ->
|
||||||
prep_stop(State) ->
|
prep_stop(State) ->
|
||||||
emqx:unhook('client.authenticate', fun emqx_auth_mnesia:check/3),
|
emqx:unhook('client.authenticate', fun emqx_auth_mnesia:check/3),
|
||||||
emqx:unhook('client.check_acl', fun emqx_acl_mnesia:check_acl/5),
|
emqx:unhook('client.check_acl', fun emqx_acl_mnesia:check_acl/5),
|
||||||
emqx_ctl:unregister_command('mqtt-user'),
|
emqx_ctl:unregister_command(clientid),
|
||||||
emqx_ctl:unregister_command('mqtt-acl'),
|
emqx_ctl:unregister_command(username),
|
||||||
|
emqx_ctl:unregister_command(user),
|
||||||
|
emqx_ctl:unregister_command(acl),
|
||||||
State.
|
State.
|
||||||
|
|
||||||
stop(_State) ->
|
stop(_State) ->
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
load_auth_hook() ->
|
load_auth_hook() ->
|
||||||
DefaultUsers = application:get_env(?APP, userlist, []),
|
ClientidList = application:get_env(?APP, clientid_list, []),
|
||||||
ok = emqx_auth_mnesia:init(DefaultUsers),
|
UsernameList = application:get_env(?APP, username_list, []),
|
||||||
|
ok = emqx_auth_mnesia:init(#{clientid_list => ClientidList, username_list => UsernameList}),
|
||||||
ok = emqx_auth_mnesia:register_metrics(),
|
ok = emqx_auth_mnesia:register_metrics(),
|
||||||
Params = #{
|
Params = #{
|
||||||
hash_type => application:get_env(emqx_auth_mnesia, hash_type, sha256),
|
hash_type => application:get_env(emqx_auth_mnesia, hash_type, sha256)
|
||||||
key_as => application:get_env(emqx_auth_mnesia, as, username)
|
|
||||||
},
|
},
|
||||||
emqx:hook('client.authenticate', fun emqx_auth_mnesia:check/3, [Params]).
|
emqx:hook('client.authenticate', fun emqx_auth_mnesia:check/3, [Params]).
|
||||||
|
|
||||||
load_acl_hook() ->
|
load_acl_hook() ->
|
||||||
ok = emqx_acl_mnesia:init(),
|
ok = emqx_acl_mnesia:init(),
|
||||||
ok = emqx_acl_mnesia:register_metrics(),
|
ok = emqx_acl_mnesia:register_metrics(),
|
||||||
Params = #{
|
emqx:hook('client.check_acl', fun emqx_acl_mnesia:check_acl/5, [#{}]).
|
||||||
key_as => application:get_env(emqx_auth_mnesia, as, username)
|
|
||||||
},
|
|
||||||
emqx:hook('client.check_acl', fun emqx_acl_mnesia:check_acl/5, [Params]).
|
|
||||||
|
|
||||||
|
|
|
@ -18,31 +18,32 @@
|
||||||
|
|
||||||
-include("emqx_auth_mnesia.hrl").
|
-include("emqx_auth_mnesia.hrl").
|
||||||
-include_lib("emqx_libs/include/logger.hrl").
|
-include_lib("emqx_libs/include/logger.hrl").
|
||||||
|
-include_lib("stdlib/include/ms_transform.hrl").
|
||||||
-define(TABLE, emqx_user).
|
-define(TABLE, emqx_user).
|
||||||
%% Auth APIs
|
%% Auth APIs
|
||||||
-export([ add_user/3
|
-export([ add_user/2
|
||||||
, update_user/3
|
, update_user/2
|
||||||
, remove_user/1
|
, remove_user/1
|
||||||
, lookup_user/1
|
, lookup_user/1
|
||||||
, all_users/0
|
, all_users/0
|
||||||
]).
|
, all_users/1
|
||||||
%% Acl APIs
|
|
||||||
-export([ add_acl/4
|
|
||||||
, remove_acl/2
|
|
||||||
, lookup_acl/1
|
|
||||||
, all_acls/0
|
|
||||||
]).
|
]).
|
||||||
%% Cli
|
%% Cli
|
||||||
-export([ auth_cli/1
|
-export([ auth_clientid_cli/1
|
||||||
, acl_cli/1]).
|
, auth_username_cli/1
|
||||||
|
]).
|
||||||
|
|
||||||
|
%% Helper
|
||||||
|
-export([comparing/2]).
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% Auth APIs
|
%% Auth APIs
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
%% @doc Add User
|
%% @doc Add User
|
||||||
-spec(add_user(binary(), binary(), atom()) -> ok | {error, any()}).
|
-spec(add_user(tuple(), binary()) -> ok | {error, any()}).
|
||||||
add_user(Login, Password, IsSuperuser) ->
|
add_user(Login, Password) ->
|
||||||
User = #emqx_user{login = Login, password = encrypted_data(Password), is_superuser = IsSuperuser},
|
User = #emqx_user{login = Login, password = encrypted_data(Password), created_at = erlang:system_time(millisecond)},
|
||||||
ret(mnesia:transaction(fun insert_user/1, [User])).
|
ret(mnesia:transaction(fun insert_user/1, [User])).
|
||||||
|
|
||||||
insert_user(User = #emqx_user{login = Login}) ->
|
insert_user(User = #emqx_user{login = Login}) ->
|
||||||
|
@ -52,30 +53,31 @@ insert_user(User = #emqx_user{login = Login}) ->
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% @doc Update User
|
%% @doc Update User
|
||||||
-spec(update_user(binary(), binary(), atom()) -> ok | {error, any()}).
|
-spec(update_user(tuple(), binary()) -> ok | {error, any()}).
|
||||||
update_user(Login, NewPassword, IsSuperuser) ->
|
update_user(Login, NewPassword) ->
|
||||||
User = #emqx_user{login = Login, password = encrypted_data(NewPassword), is_superuser = IsSuperuser},
|
User = #emqx_user{login = Login, password = encrypted_data(NewPassword)},
|
||||||
ret(mnesia:transaction(fun do_update_user/1, [User])).
|
ret(mnesia:transaction(fun do_update_user/1, [User])).
|
||||||
|
|
||||||
do_update_user(User = #emqx_user{login = Login}) ->
|
do_update_user(User = #emqx_user{login = Login}) ->
|
||||||
case mnesia:read(?TABLE, Login) of
|
case mnesia:read(?TABLE, Login) of
|
||||||
[_|_] -> mnesia:write(User);
|
[{?TABLE, Login, _, CreateAt}] -> mnesia:write(User#emqx_user{created_at = CreateAt});
|
||||||
[] -> mnesia:abort(noexisted)
|
[] -> mnesia:abort(noexisted)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% @doc Lookup user by login
|
%% @doc Lookup user by login
|
||||||
-spec(lookup_user(binary()) -> list()).
|
-spec(lookup_user(tuple()) -> list()).
|
||||||
lookup_user(undefined) -> [];
|
lookup_user(undefined) -> [];
|
||||||
lookup_user(Login) ->
|
lookup_user(Login) ->
|
||||||
case mnesia:dirty_read(?TABLE, Login) of
|
case mnesia:dirty_read(?TABLE, Login) of
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
?LOG(error, "[Mnesia] do_check_user error: ~p~n", [Reason]),
|
?LOG(error, "[Mnesia] do_check_user error: ~p~n", [Reason]),
|
||||||
[];
|
[];
|
||||||
Re -> Re
|
Re ->
|
||||||
|
lists:sort(fun comparing/2, Re)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% @doc Remove user
|
%% @doc Remove user
|
||||||
-spec(remove_user(binary()) -> ok | {error, any()}).
|
-spec(remove_user(tuple()) -> ok | {error, any()}).
|
||||||
remove_user(Login) ->
|
remove_user(Login) ->
|
||||||
ret(mnesia:transaction(fun mnesia:delete/1, [{?TABLE, Login}])).
|
ret(mnesia:transaction(fun mnesia:delete/1, [{?TABLE, Login}])).
|
||||||
|
|
||||||
|
@ -83,111 +85,97 @@ remove_user(Login) ->
|
||||||
-spec(all_users() -> list()).
|
-spec(all_users() -> list()).
|
||||||
all_users() -> mnesia:dirty_all_keys(?TABLE).
|
all_users() -> mnesia:dirty_all_keys(?TABLE).
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
all_users(clientid) ->
|
||||||
%% Acl API
|
MatchSpec = ets:fun2ms(fun({?TABLE, {clientid, Clientid}, Password, CreatedAt}) -> {?TABLE, {clientid, Clientid}, Password, CreatedAt} end),
|
||||||
%%--------------------------------------------------------------------
|
lists:sort(fun comparing/2, ets:select(?TABLE, MatchSpec));
|
||||||
|
|
||||||
%% @doc Add Acls
|
|
||||||
-spec(add_acl(binary(), binary(), binary(), atom()) -> ok | {error, any()}).
|
|
||||||
add_acl(Login, Topic, Action, Allow) ->
|
|
||||||
Acls = #emqx_acl{login = Login, topic = Topic, action = Action, allow = Allow},
|
|
||||||
ret(mnesia:transaction(fun mnesia:write/1, [Acls])).
|
|
||||||
|
|
||||||
%% @doc Lookup acl by login
|
|
||||||
-spec(lookup_acl(binary()) -> list()).
|
|
||||||
lookup_acl(undefined) -> [];
|
|
||||||
lookup_acl(Login) ->
|
|
||||||
case mnesia:dirty_read(emqx_acl, Login) of
|
|
||||||
{error, Reason} ->
|
|
||||||
?LOG(error, "[Mnesia] do_check_acl error: ~p~n", [Reason]),
|
|
||||||
[];
|
|
||||||
Re -> Re
|
|
||||||
end.
|
|
||||||
|
|
||||||
%% @doc Remove acl
|
|
||||||
-spec(remove_acl(binary(), binary()) -> ok | {error, any()}).
|
|
||||||
remove_acl(Login, Topic) ->
|
|
||||||
[ ok = mnesia:dirty_delete_object(emqx_acl, #emqx_acl{login = Login, topic = Topic, action = Action, allow = Allow}) || [Action, Allow] <- ets:select(emqx_acl, [{{emqx_acl, Login, Topic,'$1','$2'}, [], ['$$']}])],
|
|
||||||
ok.
|
|
||||||
|
|
||||||
|
|
||||||
%% @doc All logins
|
|
||||||
-spec(all_acls() -> list()).
|
|
||||||
all_acls() -> mnesia:dirty_all_keys(emqx_acl).
|
|
||||||
|
|
||||||
|
all_users(username) ->
|
||||||
|
MatchSpec = ets:fun2ms(fun({?TABLE, {username, Username}, Password, CreatedAt}) -> {?TABLE, {username, Username}, Password, CreatedAt} end),
|
||||||
|
lists:sort(fun comparing/2, ets:select(?TABLE, MatchSpec)).
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% Internal functions
|
%% Internal functions
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
comparing({?TABLE, _, _, CreatedAt1},
|
||||||
|
{?TABLE, _, _, CreatedAt2}) ->
|
||||||
|
CreatedAt1 >= CreatedAt2.
|
||||||
|
|
||||||
ret({atomic, ok}) -> ok;
|
ret({atomic, ok}) -> ok;
|
||||||
ret({aborted, Error}) -> {error, Error}.
|
ret({aborted, Error}) -> {error, Error}.
|
||||||
|
|
||||||
encrypted_data(Password) ->
|
encrypted_data(Password) ->
|
||||||
HashType = application:get_env(emqx_auth_mnesia, hash_type, sha256),
|
HashType = application:get_env(emqx_auth_mnesia, password_hash, sha256),
|
||||||
emqx_passwd:hash(HashType, Password).
|
SaltBin = salt(),
|
||||||
|
<<SaltBin/binary, (hash(Password, SaltBin, HashType))/binary>>.
|
||||||
|
|
||||||
|
hash(undefined, SaltBin, HashType) ->
|
||||||
|
hash(<<>>, SaltBin, HashType);
|
||||||
|
hash(Password, SaltBin, HashType) ->
|
||||||
|
emqx_passwd:hash(HashType, <<SaltBin/binary, Password/binary>>).
|
||||||
|
|
||||||
|
salt() ->
|
||||||
|
rand:seed(exsplus, erlang:timestamp()),
|
||||||
|
Salt = rand:uniform(16#ffffffff), <<Salt:32>>.
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% Auth APIs
|
%% Auth Clientid Cli
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
%% User
|
auth_clientid_cli(["list"]) ->
|
||||||
auth_cli(["add", Login, Password, IsSuperuser]) ->
|
[emqx_ctl:print("~s~n", [ClientId]) || {?TABLE, {clientid, ClientId}, _Password, _CreatedAt} <- all_users(clientid)];
|
||||||
case add_user(iolist_to_binary(Login), iolist_to_binary(Password), IsSuperuser) of
|
|
||||||
|
auth_clientid_cli(["add", ClientId, Password]) ->
|
||||||
|
case add_user({clientid, iolist_to_binary(ClientId)}, iolist_to_binary(Password)) of
|
||||||
ok -> emqx_ctl:print("ok~n");
|
ok -> emqx_ctl:print("ok~n");
|
||||||
{error, Reason} -> emqx_ctl:print("Error: ~p~n", [Reason])
|
{error, Reason} -> emqx_ctl:print("Error: ~p~n", [Reason])
|
||||||
end;
|
end;
|
||||||
|
|
||||||
auth_cli(["update", Login, NewPassword, IsSuperuser]) ->
|
auth_clientid_cli(["update", ClientId, NewPassword]) ->
|
||||||
case update_user(iolist_to_binary(Login), iolist_to_binary(NewPassword), IsSuperuser) of
|
case update_user({clientid, iolist_to_binary(ClientId)}, iolist_to_binary(NewPassword)) of
|
||||||
ok -> emqx_ctl:print("ok~n");
|
ok -> emqx_ctl:print("ok~n");
|
||||||
{error, Reason} -> emqx_ctl:print("Error: ~p~n", [Reason])
|
{error, Reason} -> emqx_ctl:print("Error: ~p~n", [Reason])
|
||||||
end;
|
end;
|
||||||
|
|
||||||
auth_cli(["del", Login]) ->
|
auth_clientid_cli(["del", ClientId]) ->
|
||||||
case remove_user(iolist_to_binary(Login)) of
|
case remove_user({clientid, iolist_to_binary(ClientId)}) of
|
||||||
ok -> emqx_ctl:print("ok~n");
|
ok -> emqx_ctl:print("ok~n");
|
||||||
{error, Reason} -> emqx_ctl:print("Error: ~p~n", [Reason])
|
{error, Reason} -> emqx_ctl:print("Error: ~p~n", [Reason])
|
||||||
end;
|
end;
|
||||||
|
|
||||||
auth_cli(["show", P]) ->
|
auth_clientid_cli(_) ->
|
||||||
[emqx_ctl:print("User(login = ~p is_super = ~p)~n", [Login, IsSuperuser])
|
emqx_ctl:usage([{"clientid list", "List clientid auth rules"},
|
||||||
|| {_, Login, _Password, IsSuperuser} <- lookup_user(iolist_to_binary(P))];
|
{"clientid add <Username> <Password>", "Add clientid auth rule"},
|
||||||
|
{"clientid update <Username> <NewPassword>", "Update clientid auth rule"},
|
||||||
|
{"clientid del <Username>", "Delete clientid auth rule"}]).
|
||||||
|
|
||||||
auth_cli(["list"]) ->
|
%%--------------------------------------------------------------------
|
||||||
[emqx_ctl:print("User(login = ~p)~n",[E])
|
%% Auth Username Cli
|
||||||
|| E <- all_users()];
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
auth_cli(_) ->
|
auth_username_cli(["list"]) ->
|
||||||
emqx_ctl:usage([{"mqtt-user add <Login> <Password> <IsSuper>", "Add user"},
|
[emqx_ctl:print("~s~n", [Username]) || {?TABLE, {username, Username}, _Password, _CreatedAt}<- all_users(username)];
|
||||||
{"mqtt-user update <Login> <NewPassword> <IsSuper>", "Update user"},
|
|
||||||
{"mqtt-user delete <Login>", "Delete user"},
|
|
||||||
{"mqtt-user show <Login>", "Lookup user detail"},
|
|
||||||
{"mqtt-user list", "List all users"}]).
|
|
||||||
|
|
||||||
%% Acl
|
auth_username_cli(["add", Username, Password]) ->
|
||||||
acl_cli(["add", Login, Topic, Action, Allow]) ->
|
case add_user({username, iolist_to_binary(Username)}, iolist_to_binary(Password)) of
|
||||||
case add_acl(iolist_to_binary(Login), iolist_to_binary(Topic), iolist_to_binary(Action), Allow) of
|
|
||||||
ok -> emqx_ctl:print("ok~n");
|
ok -> emqx_ctl:print("ok~n");
|
||||||
{error, Reason} -> emqx_ctl:print("Error: ~p~n", [Reason])
|
{error, Reason} -> emqx_ctl:print("Error: ~p~n", [Reason])
|
||||||
end;
|
end;
|
||||||
|
|
||||||
acl_cli(["del", Login, Topic])->
|
auth_username_cli(["update", Username, NewPassword]) ->
|
||||||
case remove_acl(iolist_to_binary(Login), iolist_to_binary(Topic)) of
|
case update_user({username, iolist_to_binary(Username)}, iolist_to_binary(NewPassword)) of
|
||||||
ok -> emqx_ctl:print("ok~n");
|
ok -> emqx_ctl:print("ok~n");
|
||||||
{error, Reason} -> emqx_ctl:print("Error: ~p~n", [Reason])
|
{error, Reason} -> emqx_ctl:print("Error: ~p~n", [Reason])
|
||||||
end;
|
end;
|
||||||
|
|
||||||
acl_cli(["show", P]) ->
|
auth_username_cli(["del", Username]) ->
|
||||||
[emqx_ctl:print("Acl(login = ~p topic = ~p action = ~p allow = ~p)~n",[Login, Topic, Action, Allow])
|
case remove_user({username, iolist_to_binary(Username)}) of
|
||||||
|| {_, Login, Topic, Action, Allow} <- lookup_acl(iolist_to_binary(P)) ];
|
ok -> emqx_ctl:print("ok~n");
|
||||||
|
{error, Reason} -> emqx_ctl:print("Error: ~p~n", [Reason])
|
||||||
|
end;
|
||||||
|
|
||||||
acl_cli(["list"]) ->
|
auth_username_cli(_) ->
|
||||||
[emqx_ctl:print("Acl(login = ~p)~n",[E])
|
emqx_ctl:usage([{"users list", "List username auth rules"},
|
||||||
|| E <- all_acls() ];
|
{"users add <Username> <Password>", "Add username auth rule"},
|
||||||
|
{"users update <Username> <NewPassword>", "Update username auth rule"},
|
||||||
acl_cli(_) ->
|
{"users del <Username>", "Delete username auth rule"}]).
|
||||||
emqx_ctl:usage([{"mqtt-acl add <Login> <Topic> <Action> <Allow>", "Add acl"},
|
|
||||||
{"mqtt-acl show <Login>", "Lookup acl detail"},
|
|
||||||
{"mqtt-acl del <Login>", "Delete acl"},
|
|
||||||
{"mqtt-acl list","List all acls"}]).
|
|
||||||
|
|
|
@ -76,154 +76,41 @@ set_special_configs(_App) ->
|
||||||
t_management(_Config) ->
|
t_management(_Config) ->
|
||||||
clean_all_acls(),
|
clean_all_acls(),
|
||||||
?assertEqual("Acl with Mnesia", emqx_acl_mnesia:description()),
|
?assertEqual("Acl with Mnesia", emqx_acl_mnesia:description()),
|
||||||
?assertEqual([], emqx_auth_mnesia_cli:all_acls()),
|
?assertEqual([], emqx_acl_mnesia_cli:all_acls()),
|
||||||
|
|
||||||
ok = emqx_auth_mnesia_cli:add_acl(<<"test_username">>, <<"Topic/A">>, <<"sub">>, true),
|
ok = emqx_acl_mnesia_cli:add_acl({clientid, <<"test_clientid">>}, <<"topic/%c">>, sub, allow),
|
||||||
ok = emqx_auth_mnesia_cli:add_acl(<<"test_username">>, <<"Topic/B">>, <<"pub">>, true),
|
ok = emqx_acl_mnesia_cli:add_acl({clientid, <<"test_clientid">>}, <<"topic/+">>, pub, deny),
|
||||||
ok = emqx_auth_mnesia_cli:add_acl(<<"test_username">>, <<"Topic/C">>, <<"pubsub">>, true),
|
ok = emqx_acl_mnesia_cli:add_acl({username, <<"test_username">>}, <<"topic/%u">>, sub, deny),
|
||||||
|
ok = emqx_acl_mnesia_cli:add_acl({username, <<"test_username">>}, <<"topic/+">>, pub, allow),
|
||||||
?assertEqual([{emqx_acl,<<"test_username">>,<<"Topic/A">>,<<"sub">>, true},
|
ok = emqx_acl_mnesia_cli:add_acl(all, <<"#">>, pubsub, deny),
|
||||||
{emqx_acl,<<"test_username">>,<<"Topic/B">>,<<"pub">>, true},
|
|
||||||
{emqx_acl,<<"test_username">>,<<"Topic/C">>,<<"pubsub">>, true}],emqx_auth_mnesia_cli:lookup_acl(<<"test_username">>)),
|
|
||||||
ok = emqx_auth_mnesia_cli:remove_acl(<<"test_username">>, <<"Topic/A">>),
|
|
||||||
?assertEqual([{emqx_acl,<<"test_username">>,<<"Topic/B">>,<<"pub">>, true},
|
|
||||||
{emqx_acl,<<"test_username">>,<<"Topic/C">>,<<"pubsub">>, true}], emqx_auth_mnesia_cli:lookup_acl(<<"test_username">>)),
|
|
||||||
|
|
||||||
|
?assertEqual(2, length(emqx_acl_mnesia_cli:lookup_acl({clientid, <<"test_clientid">>}))),
|
||||||
ok = emqx_auth_mnesia_cli:add_acl(<<"$all">>, <<"Topic/A">>, <<"sub">>, true),
|
?assertEqual(2, length(emqx_acl_mnesia_cli:lookup_acl({username, <<"test_username">>}))),
|
||||||
ok = emqx_auth_mnesia_cli:add_acl(<<"$all">>, <<"Topic/B">>, <<"pub">>, true),
|
?assertEqual(1, length(emqx_acl_mnesia_cli:lookup_acl(all))),
|
||||||
ok = emqx_auth_mnesia_cli:add_acl(<<"$all">>, <<"Topic/C">>, <<"pubsub">>, true),
|
?assertEqual(5, length(emqx_acl_mnesia_cli:all_acls())),
|
||||||
|
|
||||||
?assertEqual([{emqx_acl,<<"$all">>,<<"Topic/A">>,<<"sub">>, true},
|
|
||||||
{emqx_acl,<<"$all">>,<<"Topic/B">>,<<"pub">>, true},
|
|
||||||
{emqx_acl,<<"$all">>,<<"Topic/C">>,<<"pubsub">>, true}],emqx_auth_mnesia_cli:lookup_acl(<<"$all">>)),
|
|
||||||
ok = emqx_auth_mnesia_cli:remove_acl(<<"$all">>, <<"Topic/A">>),
|
|
||||||
?assertEqual([{emqx_acl,<<"$all">>,<<"Topic/B">>,<<"pub">>, true},
|
|
||||||
{emqx_acl,<<"$all">>,<<"Topic/C">>,<<"pubsub">>, true}], emqx_auth_mnesia_cli:lookup_acl(<<"$all">>)).
|
|
||||||
|
|
||||||
t_check_acl_as_clientid(_) ->
|
|
||||||
clean_all_acls(),
|
|
||||||
emqx_modules:load_module(emqx_mod_acl_internal, false),
|
|
||||||
|
|
||||||
User1 = #{zone => external, clientid => <<"test_clientid">>},
|
User1 = #{zone => external, clientid => <<"test_clientid">>},
|
||||||
User2 = #{zone => external, clientid => <<"no_exist">>},
|
User2 = #{zone => external, clientid => <<"no_exist">>, username => <<"test_username">>},
|
||||||
|
User3 = #{zone => external, clientid => <<"test_clientid">>, username => <<"test_username">>},
|
||||||
|
allow = emqx_access_control:check_acl(User1, subscribe, <<"topic/test_clientid">>),
|
||||||
|
deny = emqx_access_control:check_acl(User1, publish, <<"topic/A">>),
|
||||||
|
deny = emqx_access_control:check_acl(User2, subscribe, <<"topic/test_username">>),
|
||||||
|
allow = emqx_access_control:check_acl(User2, publish, <<"topic/A">>),
|
||||||
|
allow = emqx_access_control:check_acl(User3, subscribe, <<"topic/test_clientid">>),
|
||||||
|
deny = emqx_access_control:check_acl(User3, subscribe, <<"topic/test_username">>),
|
||||||
|
deny = emqx_access_control:check_acl(User3, publish, <<"topic/A">>),
|
||||||
|
deny = emqx_access_control:check_acl(User3, subscribe, <<"topic/A/B">>),
|
||||||
|
deny = emqx_access_control:check_acl(User3, publish, <<"topic/A/B">>),
|
||||||
|
|
||||||
ok = emqx_auth_mnesia_cli:add_acl(<<"test_clientid">>, <<"#">>, <<"sub">>, false),
|
ok = emqx_acl_mnesia_cli:remove_acl({clientid, <<"test_clientid">>}, <<"topic/%c">>),
|
||||||
ok = emqx_auth_mnesia_cli:add_acl(<<"test_clientid">>, <<"+/A">>, <<"pub">>, false),
|
ok = emqx_acl_mnesia_cli:remove_acl({clientid, <<"test_clientid">>}, <<"topic/+">>),
|
||||||
ok = emqx_auth_mnesia_cli:add_acl(<<"test_clientid">>, <<"Topic/A/B">>, <<"pubsub">>, true),
|
ok = emqx_acl_mnesia_cli:remove_acl({username, <<"test_username">>}, <<"topic/%u">>),
|
||||||
|
ok = emqx_acl_mnesia_cli:remove_acl({username, <<"test_username">>}, <<"topic/+">>),
|
||||||
|
ok = emqx_acl_mnesia_cli:remove_acl(all, <<"#">>),
|
||||||
|
|
||||||
deny = emqx_access_control:check_acl(User1, subscribe, <<"Any">>),
|
?assertEqual([], emqx_acl_mnesia_cli:all_acls()).
|
||||||
deny = emqx_access_control:check_acl(User1, publish, <<"Any/A">>),
|
|
||||||
allow = emqx_access_control:check_acl(User1, publish, <<"Any/C">>),
|
|
||||||
allow = emqx_access_control:check_acl(User1, publish, <<"Topic/A/B">>),
|
|
||||||
|
|
||||||
allow = emqx_access_control:check_acl(User2, subscribe, <<"Topic/C">>),
|
t_acl_cli(_Config) ->
|
||||||
allow = emqx_access_control:check_acl(User2, publish, <<"Topic/D">>).
|
|
||||||
|
|
||||||
t_check_acl_as_username(_Config) ->
|
|
||||||
clean_all_acls(),
|
|
||||||
emqx_modules:load_module(emqx_mod_acl_internal, false),
|
|
||||||
|
|
||||||
User1 = #{zone => external, username => <<"test_username">>},
|
|
||||||
User2 = #{zone => external, username => <<"no_exist">>},
|
|
||||||
|
|
||||||
ok = emqx_auth_mnesia_cli:add_acl(<<"test_username">>, <<"Topic/A">>, <<"sub">>, true),
|
|
||||||
ok = emqx_auth_mnesia_cli:add_acl(<<"test_username">>, <<"Topic/B">>, <<"pub">>, true),
|
|
||||||
ok = emqx_auth_mnesia_cli:add_acl(<<"test_username">>, <<"Topic/A/B">>, <<"pubsub">>, false),
|
|
||||||
allow = emqx_access_control:check_acl(User1, subscribe, <<"Topic/A">>),
|
|
||||||
allow = emqx_access_control:check_acl(User1, subscribe, <<"Topic/B">>),
|
|
||||||
deny = emqx_access_control:check_acl(User1, subscribe, <<"Topic/A/B">>),
|
|
||||||
allow = emqx_access_control:check_acl(User1, publish, <<"Topic/A">>),
|
|
||||||
allow = emqx_access_control:check_acl(User1, publish, <<"Topic/B">>),
|
|
||||||
deny = emqx_access_control:check_acl(User1, publish, <<"Topic/A/B">>),
|
|
||||||
|
|
||||||
allow = emqx_access_control:check_acl(User2, subscribe, <<"Topic/C">>),
|
|
||||||
allow = emqx_access_control:check_acl(User2, publish, <<"Topic/D">>).
|
|
||||||
|
|
||||||
t_check_acl_as_all(_) ->
|
|
||||||
clean_all_acls(),
|
|
||||||
emqx_modules:load_module(emqx_mod_acl_internal, false),
|
|
||||||
|
|
||||||
ok = emqx_auth_mnesia_cli:add_acl(<<"$all">>, <<"Topic/A">>, <<"sub">>, false),
|
|
||||||
ok = emqx_auth_mnesia_cli:add_acl(<<"$all">>, <<"Topic/B">>, <<"pub">>, false),
|
|
||||||
ok = emqx_auth_mnesia_cli:add_acl(<<"$all">>, <<"Topic/A/B">>, <<"pubsub">>, true),
|
|
||||||
|
|
||||||
User1 = #{zone => external, username => <<"test_username">>},
|
|
||||||
User2 = #{zone => external, username => <<"no_exist">>},
|
|
||||||
|
|
||||||
ok = emqx_auth_mnesia_cli:add_acl(<<"test_username">>, <<"Topic/A">>, <<"sub">>, true),
|
|
||||||
ok = emqx_auth_mnesia_cli:add_acl(<<"test_username">>, <<"Topic/B">>, <<"pub">>, true),
|
|
||||||
ok = emqx_auth_mnesia_cli:add_acl(<<"test_username">>, <<"Topic/A/B">>, <<"pubsub">>, false),
|
|
||||||
|
|
||||||
allow = emqx_access_control:check_acl(User1, subscribe, <<"Topic/A">>),
|
|
||||||
allow = emqx_access_control:check_acl(User1, subscribe, <<"Topic/B">>),
|
|
||||||
deny = emqx_access_control:check_acl(User1, subscribe, <<"Topic/A/B">>),
|
|
||||||
allow = emqx_access_control:check_acl(User1, publish, <<"Topic/A">>),
|
|
||||||
allow = emqx_access_control:check_acl(User1, publish, <<"Topic/B">>),
|
|
||||||
deny = emqx_access_control:check_acl(User1, publish, <<"Topic/A/B">>),
|
|
||||||
|
|
||||||
deny = emqx_access_control:check_acl(User2, subscribe, <<"Topic/A">>),
|
|
||||||
deny = emqx_access_control:check_acl(User2, publish, <<"Topic/B">>),
|
|
||||||
allow = emqx_access_control:check_acl(User2, subscribe, <<"Topic/A/B">>),
|
|
||||||
allow = emqx_access_control:check_acl(User2, publish, <<"Topic/A/B">>),
|
|
||||||
allow = emqx_access_control:check_acl(User2, subscribe, <<"Topic/C">>),
|
|
||||||
allow = emqx_access_control:check_acl(User2, publish, <<"Topic/D">>).
|
|
||||||
|
|
||||||
t_rest_api(_Config) ->
|
|
||||||
clean_all_acls(),
|
|
||||||
|
|
||||||
{ok, Result} = request_http_rest_list(),
|
|
||||||
[] = get_http_data(Result),
|
|
||||||
|
|
||||||
Params = #{<<"login">> => <<"test_username">>, <<"topic">> => <<"Topic/A">>, <<"action">> => <<"pubsub">>, <<"allow">> => true},
|
|
||||||
{ok, _} = request_http_rest_add(Params),
|
|
||||||
{ok, Result1} = request_http_rest_lookup(<<"test_username">>),
|
|
||||||
#{<<"login">> := <<"test_username">>, <<"topic">> := <<"Topic/A">>, <<"action">> := <<"pubsub">>, <<"allow">> := true} = get_http_data(Result1),
|
|
||||||
|
|
||||||
Params1 = [
|
|
||||||
#{<<"login">> => <<"$all">>, <<"topic">> => <<"+/A">>, <<"action">> => <<"pub">>, <<"allow">> => true},
|
|
||||||
#{<<"login">> => <<"test_username">>, <<"topic">> => <<"+/A">>, <<"action">> => <<"pub">>, <<"allow">> => true},
|
|
||||||
#{<<"login">> => <<"test_username/1">>, <<"topic">> => <<"#">>, <<"action">> => <<"sub">>, <<"allow">> => true},
|
|
||||||
#{<<"login">> => <<"test_username/2">>, <<"topic">> => <<"+/A">>, <<"action">> => <<"error_format">>, <<"allow">> => true}
|
|
||||||
],
|
|
||||||
{ok, Result2} = request_http_rest_add(Params1),
|
|
||||||
#{
|
|
||||||
<<"$all">> := <<"ok">>,
|
|
||||||
<<"test_username">> := <<"ok">>,
|
|
||||||
<<"test_username/1">> := <<"ok">>,
|
|
||||||
<<"test_username/2">> := <<"{error,action}">>
|
|
||||||
} = get_http_data(Result2),
|
|
||||||
|
|
||||||
{ok, Result3} = request_http_rest_lookup(<<"test_username">>),
|
|
||||||
[#{<<"login">> := <<"test_username">>, <<"topic">> := <<"+/A">>, <<"action">> := <<"pub">>, <<"allow">> := true},
|
|
||||||
#{<<"login">> := <<"test_username">>, <<"topic">> := <<"Topic/A">>, <<"action">> := <<"pubsub">>, <<"allow">> := true}]
|
|
||||||
= get_http_data(Result3),
|
|
||||||
|
|
||||||
{ok, Result4} = request_http_rest_lookup(<<"$all">>),
|
|
||||||
#{<<"login">> := <<"$all">>, <<"topic">> := <<"+/A">>, <<"action">> := <<"pub">>, <<"allow">> := true}
|
|
||||||
= get_http_data(Result4),
|
|
||||||
|
|
||||||
{ok, _} = request_http_rest_delete(<<"$all">>, <<"+/A">>),
|
|
||||||
{ok, _} = request_http_rest_delete(<<"test_username">>, <<"+/A">>),
|
|
||||||
{ok, _} = request_http_rest_delete(<<"test_username">>, <<"Topic/A">>),
|
|
||||||
{ok, _} = request_http_rest_delete(<<"test_username/1">>, <<"#">>),
|
|
||||||
{ok, Result5} = request_http_rest_list(),
|
|
||||||
[] = get_http_data(Result5).
|
|
||||||
|
|
||||||
|
|
||||||
t_run_command(_) ->
|
|
||||||
clean_all_acls(),
|
|
||||||
?assertEqual(ok, emqx_ctl:run_command(["mqtt-acl", "add", "TestUser", "Topic/A", "sub", true])),
|
|
||||||
?assertEqual([{emqx_acl,<<"TestUser">>,<<"Topic/A">>,<<"sub">>, true}],emqx_auth_mnesia_cli:lookup_acl(<<"TestUser">>)),
|
|
||||||
|
|
||||||
?assertEqual(ok, emqx_ctl:run_command(["mqtt-acl", "del", "TestUser", "Topic/A"])),
|
|
||||||
?assertEqual([],emqx_auth_mnesia_cli:lookup_acl(<<"TestUser">>)),
|
|
||||||
|
|
||||||
?assertEqual(ok, emqx_ctl:run_command(["mqtt-acl", "show", "TestUser"])),
|
|
||||||
?assertEqual(ok, emqx_ctl:run_command(["mqtt-acl", "list"])),
|
|
||||||
?assertEqual(ok, emqx_ctl:run_command(["mqtt-acl"])).
|
|
||||||
|
|
||||||
t_cli(_) ->
|
|
||||||
meck:new(emqx_ctl, [non_strict, passthrough]),
|
meck:new(emqx_ctl, [non_strict, passthrough]),
|
||||||
meck:expect(emqx_ctl, print, fun(Arg) -> emqx_ctl:format(Arg) end),
|
meck:expect(emqx_ctl, print, fun(Arg) -> emqx_ctl:format(Arg) end),
|
||||||
meck:expect(emqx_ctl, print, fun(Msg, Arg) -> emqx_ctl:format(Msg, Arg) end),
|
meck:expect(emqx_ctl, print, fun(Msg, Arg) -> emqx_ctl:format(Msg, Arg) end),
|
||||||
|
@ -231,18 +118,67 @@ t_cli(_) ->
|
||||||
meck:expect(emqx_ctl, usage, fun(Cmd, Descr) -> emqx_ctl:format_usage(Cmd, Descr) end),
|
meck:expect(emqx_ctl, usage, fun(Cmd, Descr) -> emqx_ctl:format_usage(Cmd, Descr) end),
|
||||||
|
|
||||||
clean_all_acls(),
|
clean_all_acls(),
|
||||||
?assertMatch({match, _}, re:run(emqx_auth_mnesia_cli:acl_cli(["add", "TestUser", "Topic/A", "sub", true]), "ok")),
|
|
||||||
?assertMatch(["Acl(login = <<\"TestUser\">> topic = <<\"Topic/A\">> action = <<\"sub\">> allow = true)\n"], emqx_auth_mnesia_cli:acl_cli(["show", "TestUser"])),
|
|
||||||
?assertMatch(["Acl(login = <<\"TestUser\">>)\n"], emqx_auth_mnesia_cli:acl_cli(["list"])),
|
|
||||||
|
|
||||||
?assertMatch({match, _}, re:run(emqx_auth_mnesia_cli:acl_cli(["del", "TestUser", "Topic/A"]), "ok")),
|
?assertEqual(0, length(emqx_acl_mnesia_cli:cli(["list"]))),
|
||||||
?assertMatch([], emqx_auth_mnesia_cli:acl_cli(["show", "TestUser"])),
|
|
||||||
?assertMatch([], emqx_auth_mnesia_cli:acl_cli(["list"])),
|
|
||||||
|
|
||||||
?assertMatch({match, _}, re:run(emqx_auth_mnesia_cli:acl_cli([]), "mqtt-acl")),
|
emqx_acl_mnesia_cli:cli(["add", "clientid", "test_clientid", "topic/A", "pub", "allow"]),
|
||||||
|
?assertMatch(["Acl(clientid = <<\"test_clientid\">> topic = <<\"topic/A\">> action = pub access = allow)\n"], emqx_acl_mnesia_cli:cli(["show", "clientid", "test_clientid"])),
|
||||||
|
?assertMatch(["Acl(clientid = <<\"test_clientid\">> topic = <<\"topic/A\">> action = pub access = allow)\n"], emqx_acl_mnesia_cli:cli(["list", "clientid"])),
|
||||||
|
|
||||||
|
emqx_acl_mnesia_cli:cli(["add", "username", "test_username", "topic/B", "sub", "deny"]),
|
||||||
|
?assertMatch(["Acl(username = <<\"test_username\">> topic = <<\"topic/B\">> action = sub access = deny)\n"], emqx_acl_mnesia_cli:cli(["show", "username", "test_username"])),
|
||||||
|
?assertMatch(["Acl(username = <<\"test_username\">> topic = <<\"topic/B\">> action = sub access = deny)\n"], emqx_acl_mnesia_cli:cli(["list", "username"])),
|
||||||
|
|
||||||
|
emqx_acl_mnesia_cli:cli(["add", "_all", "#", "pubsub", "deny"]),
|
||||||
|
?assertMatch(["Acl($all topic = <<\"#\">> action = pubsub access = deny)\n"], emqx_acl_mnesia_cli:cli(["list", "_all"])),
|
||||||
|
?assertEqual(3, length(emqx_acl_mnesia_cli:cli(["list"]))),
|
||||||
|
|
||||||
|
emqx_acl_mnesia_cli:cli(["del", "clientid", "test_clientid", "topic/A"]),
|
||||||
|
emqx_acl_mnesia_cli:cli(["del", "username", "test_username", "topic/B"]),
|
||||||
|
emqx_acl_mnesia_cli:cli(["del", "_all", "#"]),
|
||||||
|
?assertEqual(0, length(emqx_acl_mnesia_cli:cli(["list"]))),
|
||||||
|
|
||||||
meck:unload(emqx_ctl).
|
meck:unload(emqx_ctl).
|
||||||
|
|
||||||
|
t_rest_api(_Config) ->
|
||||||
|
clean_all_acls(),
|
||||||
|
|
||||||
|
Params1 = [#{<<"clientid">> => <<"test_clientid">>, <<"topic">> => <<"topic/A">>, <<"action">> => <<"pub">>, <<"access">> => <<"allow">>},
|
||||||
|
#{<<"clientid">> => <<"test_clientid">>, <<"topic">> => <<"topic/B">>, <<"action">> => <<"sub">>, <<"access">> => <<"allow">>},
|
||||||
|
#{<<"clientid">> => <<"test_clientid">>, <<"topic">> => <<"topic/C">>, <<"action">> => <<"pubsub">>, <<"access">> => <<"deny">>}],
|
||||||
|
{ok, _} = request_http_rest_add([], Params1),
|
||||||
|
{ok, Re1} = request_http_rest_list(["clientid", "test_clientid"]),
|
||||||
|
?assertMatch(3, length(get_http_data(Re1))),
|
||||||
|
{ok, _} = request_http_rest_delete(["clientid", "test_clientid", "topic", "topic/A"]),
|
||||||
|
{ok, _} = request_http_rest_delete(["clientid", "test_clientid", "topic", "topic/B"]),
|
||||||
|
{ok, _} = request_http_rest_delete(["clientid", "test_clientid", "topic", "topic/C"]),
|
||||||
|
{ok, Res1} = request_http_rest_list(["clientid"]),
|
||||||
|
?assertMatch([], get_http_data(Res1)),
|
||||||
|
|
||||||
|
Params2 = [#{<<"username">> => <<"test_username">>, <<"topic">> => <<"topic/A">>, <<"action">> => <<"pub">>, <<"access">> => <<"allow">>},
|
||||||
|
#{<<"username">> => <<"test_username">>, <<"topic">> => <<"topic/B">>, <<"action">> => <<"sub">>, <<"access">> => <<"allow">>},
|
||||||
|
#{<<"username">> => <<"test_username">>, <<"topic">> => <<"topic/C">>, <<"action">> => <<"pubsub">>, <<"access">> => <<"deny">>}],
|
||||||
|
{ok, _} = request_http_rest_add([], Params2),
|
||||||
|
{ok, Re2} = request_http_rest_list(["username", "test_username"]),
|
||||||
|
?assertMatch(3, length(get_http_data(Re2))),
|
||||||
|
{ok, _} = request_http_rest_delete(["username", "test_username", "topic", "topic/A"]),
|
||||||
|
{ok, _} = request_http_rest_delete(["username", "test_username", "topic", "topic/B"]),
|
||||||
|
{ok, _} = request_http_rest_delete(["username", "test_username", "topic", "topic/C"]),
|
||||||
|
{ok, Res2} = request_http_rest_list(["username"]),
|
||||||
|
?assertMatch([], get_http_data(Res2)),
|
||||||
|
|
||||||
|
Params3 = [#{<<"topic">> => <<"topic/A">>, <<"action">> => <<"pub">>, <<"access">> => <<"allow">>},
|
||||||
|
#{<<"topic">> => <<"topic/B">>, <<"action">> => <<"sub">>, <<"access">> => <<"allow">>},
|
||||||
|
#{<<"topic">> => <<"topic/C">>, <<"action">> => <<"pubsub">>, <<"access">> => <<"deny">>}],
|
||||||
|
{ok, _} = request_http_rest_add([], Params3),
|
||||||
|
{ok, Re3} = request_http_rest_list(["$all"]),
|
||||||
|
?assertMatch(3, length(get_http_data(Re3))),
|
||||||
|
{ok, _} = request_http_rest_delete(["$all", "topic", "topic/A"]),
|
||||||
|
{ok, _} = request_http_rest_delete(["$all", "topic", "topic/B"]),
|
||||||
|
{ok, _} = request_http_rest_delete(["$all", "topic", "topic/C"]),
|
||||||
|
{ok, Res3} = request_http_rest_list(["$all"]),
|
||||||
|
?assertMatch([], get_http_data(Res3)).
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
%% Helpers
|
%% Helpers
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
@ -255,22 +191,22 @@ clean_all_acls() ->
|
||||||
%% HTTP Request
|
%% HTTP Request
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
request_http_rest_list() ->
|
request_http_rest_list(Path) ->
|
||||||
request_api(get, uri(), default_auth_header()).
|
request_api(get, uri(Path), default_auth_header()).
|
||||||
|
|
||||||
request_http_rest_lookup(Login) ->
|
request_http_rest_lookup(Path) ->
|
||||||
request_api(get, uri([Login]), default_auth_header()).
|
request_api(get, uri(Path), default_auth_header()).
|
||||||
|
|
||||||
request_http_rest_add(Params) ->
|
request_http_rest_add(Path, Params) ->
|
||||||
request_api(post, uri(), [], default_auth_header(), Params).
|
request_api(post, uri(Path), [], default_auth_header(), Params).
|
||||||
|
|
||||||
request_http_rest_delete(Login, Topic) ->
|
request_http_rest_delete(Path) ->
|
||||||
request_api(delete, uri([Login, Topic]), default_auth_header()).
|
request_api(delete, uri(Path), default_auth_header()).
|
||||||
|
|
||||||
uri() -> uri([]).
|
uri() -> uri([]).
|
||||||
uri(Parts) when is_list(Parts) ->
|
uri(Parts) when is_list(Parts) ->
|
||||||
NParts = [b2l(E) || E <- Parts],
|
NParts = [b2l(E) || E <- Parts],
|
||||||
?HOST ++ filename:join([?BASE_PATH, ?API_VERSION, "mqtt_acl"| NParts]).
|
?HOST ++ filename:join([?BASE_PATH, ?API_VERSION, "acl"| NParts]).
|
||||||
|
|
||||||
%% @private
|
%% @private
|
||||||
b2l(B) when is_binary(B) ->
|
b2l(B) when is_binary(B) ->
|
||||||
|
|
|
@ -2,10 +2,7 @@
|
||||||
%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved.
|
%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
%%
|
%%
|
||||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
%% you may not use this file except in compliance with 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
|
||||||
%% 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
|
%% Unless required by applicable law or agreed to in writing, software
|
||||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
@ -33,6 +30,12 @@
|
||||||
-define(API_VERSION, "v4").
|
-define(API_VERSION, "v4").
|
||||||
-define(BASE_PATH, "api").
|
-define(BASE_PATH, "api").
|
||||||
|
|
||||||
|
-define(TABLE, emqx_user).
|
||||||
|
-define(CLIENTID, <<"clientid_for_ct">>).
|
||||||
|
-define(USERNAME, <<"username_for_ct">>).
|
||||||
|
-define(PASSWORD, <<"password">>).
|
||||||
|
-define(NPASSWORD, <<"new_password">>).
|
||||||
|
|
||||||
all() ->
|
all() ->
|
||||||
emqx_ct:all(?MODULE).
|
emqx_ct:all(?MODULE).
|
||||||
|
|
||||||
|
@ -81,143 +84,166 @@ set_special_configs(_App) ->
|
||||||
%% Testcases
|
%% Testcases
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
t_check_as_username(_Config) ->
|
t_management(_Config) ->
|
||||||
clean_all_users(),
|
clean_all_users(),
|
||||||
|
|
||||||
ok = emqx_auth_mnesia_cli:add_user(<<"test_username">>, <<"password">>, true),
|
ok = emqx_auth_mnesia_cli:add_user({username,?USERNAME}, ?PASSWORD),
|
||||||
{error, existed} = emqx_auth_mnesia_cli:add_user(<<"test_username">>, <<"password">>, true),
|
{error, existed} = emqx_auth_mnesia_cli:add_user({username,?USERNAME}, ?PASSWORD),
|
||||||
|
?assertMatch([{?TABLE, {username, ?USERNAME}, _Password, _InterTime}], emqx_auth_mnesia_cli:all_users(username)),
|
||||||
|
|
||||||
ok = emqx_auth_mnesia_cli:update_user(<<"test_username">>, <<"new_password">>, false),
|
ok = emqx_auth_mnesia_cli:add_user({clientid,?CLIENTID}, ?PASSWORD),
|
||||||
{error,noexisted} = emqx_auth_mnesia_cli:update_user(<<"no_existed_user">>, <<"password">>, true),
|
{error, existed} = emqx_auth_mnesia_cli:add_user({clientid,?CLIENTID}, ?PASSWORD),
|
||||||
|
?assertMatch([{?TABLE, {clientid, ?CLIENTID}, _Password, _InterTime}], emqx_auth_mnesia_cli:all_users(clientid)),
|
||||||
|
|
||||||
[<<"test_username">>] = emqx_auth_mnesia_cli:all_users(),
|
?assertEqual(2,length(emqx_auth_mnesia_cli:all_users())),
|
||||||
[{emqx_user, <<"test_username">>, _HashedPass, false}] =
|
|
||||||
emqx_auth_mnesia_cli:lookup_user(<<"test_username">>),
|
|
||||||
|
|
||||||
User1 = #{username => <<"test_username">>,
|
ok = emqx_auth_mnesia_cli:update_user({username,?USERNAME}, ?NPASSWORD),
|
||||||
password => <<"new_password">>,
|
{error,noexisted} = emqx_auth_mnesia_cli:update_user({username, <<"no_existed_user">>}, ?PASSWORD),
|
||||||
|
|
||||||
|
ok = emqx_auth_mnesia_cli:update_user({clientid,?CLIENTID}, ?NPASSWORD),
|
||||||
|
{error,noexisted} = emqx_auth_mnesia_cli:update_user({clientid, <<"no_existed_user">>}, ?PASSWORD),
|
||||||
|
|
||||||
|
|
||||||
|
?assertMatch([{?TABLE, {username, ?USERNAME}, _Password, _InterTime}], emqx_auth_mnesia_cli:lookup_user({username, ?USERNAME})),
|
||||||
|
?assertMatch([{?TABLE, {clientid, ?CLIENTID}, _Password, _InterTime}], emqx_auth_mnesia_cli:lookup_user({clientid, ?CLIENTID})),
|
||||||
|
|
||||||
|
User1 = #{username => ?USERNAME,
|
||||||
|
clientid => undefined,
|
||||||
|
password => ?NPASSWORD,
|
||||||
zone => external},
|
zone => external},
|
||||||
|
|
||||||
{ok, #{is_superuser := false,
|
{ok, #{auth_result := success,
|
||||||
auth_result := success,
|
|
||||||
anonymous := false}} = emqx_access_control:authenticate(User1),
|
anonymous := false}} = emqx_access_control:authenticate(User1),
|
||||||
|
|
||||||
{error,password_error} = emqx_access_control:authenticate(User1#{password => <<"error_password">>}),
|
{error,password_error} = emqx_access_control:authenticate(User1#{password => <<"error_password">>}),
|
||||||
|
|
||||||
ok = emqx_auth_mnesia_cli:remove_user(<<"test_username">>),
|
ok = emqx_auth_mnesia_cli:remove_user({username,?USERNAME}),
|
||||||
{ok, #{auth_result := success,
|
{ok, #{auth_result := success,
|
||||||
anonymous := true }} = emqx_access_control:authenticate(User1).
|
anonymous := true }} = emqx_access_control:authenticate(User1),
|
||||||
|
|
||||||
t_check_as_clientid(_Config) ->
|
User2 = #{clientid => ?CLIENTID,
|
||||||
clean_all_users(),
|
password => ?NPASSWORD,
|
||||||
|
|
||||||
ok = emqx_auth_mnesia_cli:add_user(<<"test_clientid">>, <<"password">>, false),
|
|
||||||
{error, existed} = emqx_auth_mnesia_cli:add_user(<<"test_clientid">>, <<"password">>, false),
|
|
||||||
|
|
||||||
ok = emqx_auth_mnesia_cli:update_user(<<"test_clientid">>, <<"new_password">>, true),
|
|
||||||
{error,noexisted} = emqx_auth_mnesia_cli:update_user(<<"no_existed_user">>, <<"password">>, true),
|
|
||||||
|
|
||||||
[<<"test_clientid">>] = emqx_auth_mnesia_cli:all_users(),
|
|
||||||
[{emqx_user, <<"test_clientid">>, _HashedPass, true}] =
|
|
||||||
emqx_auth_mnesia_cli:lookup_user(<<"test_clientid">>),
|
|
||||||
|
|
||||||
User1 = #{clientid => <<"test_clientid">>,
|
|
||||||
password => <<"new_password">>,
|
|
||||||
zone => external},
|
zone => external},
|
||||||
|
|
||||||
{ok, #{is_superuser := true,
|
|
||||||
auth_result := success,
|
|
||||||
anonymous := false}} = emqx_access_control:authenticate(User1),
|
|
||||||
|
|
||||||
{error,password_error} = emqx_access_control:authenticate(User1#{password => <<"error_password">>}),
|
|
||||||
|
|
||||||
ok = emqx_auth_mnesia_cli:remove_user(<<"test_clientid">>),
|
|
||||||
{ok, #{auth_result := success,
|
{ok, #{auth_result := success,
|
||||||
anonymous := true }} = emqx_access_control:authenticate(User1).
|
anonymous := false}} = emqx_access_control:authenticate(User2),
|
||||||
|
|
||||||
t_rest_api(_Config) ->
|
{error,password_error} = emqx_access_control:authenticate(User2#{password => <<"error_password">>}),
|
||||||
|
|
||||||
|
ok = emqx_auth_mnesia_cli:remove_user({clientid,?CLIENTID}),
|
||||||
|
{ok, #{auth_result := success,
|
||||||
|
anonymous := true }} = emqx_access_control:authenticate(User2),
|
||||||
|
|
||||||
|
[] = emqx_auth_mnesia_cli:all_users().
|
||||||
|
|
||||||
|
t_auth_clientid_cli(_) ->
|
||||||
clean_all_users(),
|
clean_all_users(),
|
||||||
|
|
||||||
{ok, Result1} = request_http_rest_list(),
|
HashType = application:get_env(emqx_auth_mnesia, password_hash, sha256),
|
||||||
|
|
||||||
|
emqx_auth_mnesia_cli:auth_clientid_cli(["add", ?CLIENTID, ?PASSWORD]),
|
||||||
|
[{_, {clientid, ?CLIENTID}, <<Salt:4/binary, Hash/binary>>, _}] = emqx_auth_mnesia_cli:lookup_user({clientid, ?CLIENTID}),
|
||||||
|
?assertEqual(Hash, emqx_passwd:hash(HashType, <<Salt/binary, ?PASSWORD/binary>>)),
|
||||||
|
|
||||||
|
emqx_auth_mnesia_cli:auth_clientid_cli(["update", ?CLIENTID, ?NPASSWORD]),
|
||||||
|
[{_, {clientid, ?CLIENTID}, <<Salt1:4/binary, Hash1/binary>>, _}] = emqx_auth_mnesia_cli:lookup_user({clientid, ?CLIENTID}),
|
||||||
|
?assertEqual(Hash1, emqx_passwd:hash(HashType, <<Salt1/binary, ?NPASSWORD/binary>>)),
|
||||||
|
|
||||||
|
emqx_auth_mnesia_cli:auth_clientid_cli(["del", ?CLIENTID]),
|
||||||
|
?assertEqual([], emqx_auth_mnesia_cli:lookup_user(?CLIENTID)),
|
||||||
|
|
||||||
|
emqx_auth_mnesia_cli:auth_clientid_cli(["add", "user1", "pass1"]),
|
||||||
|
emqx_auth_mnesia_cli:auth_clientid_cli(["add", "user2", "pass2"]),
|
||||||
|
?assertEqual(2, length(emqx_auth_mnesia_cli:auth_clientid_cli(["list"]))),
|
||||||
|
|
||||||
|
emqx_auth_mnesia_cli:auth_clientid_cli(usage).
|
||||||
|
|
||||||
|
t_auth_username_cli(_) ->
|
||||||
|
clean_all_users(),
|
||||||
|
|
||||||
|
HashType = application:get_env(emqx_auth_mnesia, password_hash, sha256),
|
||||||
|
|
||||||
|
emqx_auth_mnesia_cli:auth_username_cli(["add", ?USERNAME, ?PASSWORD]),
|
||||||
|
[{_, {username, ?USERNAME}, <<Salt:4/binary, Hash/binary>>, _}] = emqx_auth_mnesia_cli:lookup_user({username, ?USERNAME}),
|
||||||
|
?assertEqual(Hash, emqx_passwd:hash(HashType, <<Salt/binary, ?PASSWORD/binary>>)),
|
||||||
|
|
||||||
|
emqx_auth_mnesia_cli:auth_username_cli(["update", ?USERNAME, ?NPASSWORD]),
|
||||||
|
[{_, {username, ?USERNAME}, <<Salt1:4/binary, Hash1/binary>>, _}] = emqx_auth_mnesia_cli:lookup_user({username, ?USERNAME}),
|
||||||
|
?assertEqual(Hash1, emqx_passwd:hash(HashType, <<Salt1/binary, ?NPASSWORD/binary>>)),
|
||||||
|
|
||||||
|
emqx_auth_mnesia_cli:auth_username_cli(["del", ?USERNAME]),
|
||||||
|
?assertEqual([], emqx_auth_mnesia_cli:lookup_user(?USERNAME)),
|
||||||
|
|
||||||
|
emqx_auth_mnesia_cli:auth_username_cli(["add", "user1", "pass1"]),
|
||||||
|
emqx_auth_mnesia_cli:auth_username_cli(["add", "user2", "pass2"]),
|
||||||
|
?assertEqual(2, length(emqx_auth_mnesia_cli:auth_username_cli(["list"]))),
|
||||||
|
|
||||||
|
emqx_auth_mnesia_cli:auth_username_cli(usage).
|
||||||
|
|
||||||
|
|
||||||
|
t_clientid_rest_api(_Config) ->
|
||||||
|
clean_all_users(),
|
||||||
|
|
||||||
|
{ok, Result1} = request_http_rest_list(["auth_clientid"]),
|
||||||
[] = get_http_data(Result1),
|
[] = get_http_data(Result1),
|
||||||
|
|
||||||
Params = #{<<"login">> => <<"test_username">>, <<"password">> => <<"password">>, <<"is_superuser">> => true},
|
Params1 = #{<<"clientid">> => ?CLIENTID, <<"password">> => ?PASSWORD},
|
||||||
{ok, _} = request_http_rest_add(Params),
|
{ok, _} = request_http_rest_add(["auth_clientid"], Params1),
|
||||||
|
|
||||||
Params1 = [
|
Params2 = #{<<"clientid">> => ?CLIENTID, <<"password">> => ?NPASSWORD},
|
||||||
#{<<"login">> => <<"test_username">>, <<"password">> => <<"password">>, <<"is_superuser">> => true},
|
{ok, _} = request_http_rest_update(["auth_clientid/" ++ binary_to_list(?CLIENTID)], Params2),
|
||||||
#{<<"login">> => <<"test_username/1">>, <<"password">> => <<"password">>, <<"is_superuser">> => error_format},
|
|
||||||
#{<<"login">> => <<"test_username/2">>, <<"password">> => <<"password">>, <<"is_superuser">> => true}
|
{ok, Result2} = request_http_rest_lookup(["auth_clientid/" ++ binary_to_list(?CLIENTID)]),
|
||||||
],
|
?assertMatch(#{<<"clientid">> := ?CLIENTID}, get_http_data(Result2)),
|
||||||
{ok, Result2} = request_http_rest_add(Params1),
|
|
||||||
#{
|
|
||||||
<<"test_username">> := <<"{error,existed}">>,
|
|
||||||
<<"test_username/1">> := <<"{error,is_superuser}">>,
|
|
||||||
<<"test_username/2">> := <<"ok">>
|
|
||||||
} = get_http_data(Result2),
|
|
||||||
|
|
||||||
{ok, Result3} = request_http_rest_lookup(<<"test_username">>),
|
Params3 = [ #{<<"clientid">> => ?CLIENTID, <<"password">> => ?PASSWORD}
|
||||||
#{<<"login">> := <<"test_username">>, <<"is_superuser">> := true} = get_http_data(Result3),
|
, #{<<"clientid">> => <<"clientid1">>, <<"password">> => ?PASSWORD}
|
||||||
|
, #{<<"clientid">> => <<"clientid2">>, <<"password">> => ?PASSWORD}
|
||||||
|
],
|
||||||
|
{ok, Result3} = request_http_rest_add(["auth_clientid"], Params3),
|
||||||
|
?assertMatch(#{ ?CLIENTID := <<"{error,existed}">>
|
||||||
|
, <<"clientid1">> := <<"ok">>
|
||||||
|
, <<"clientid2">> := <<"ok">>
|
||||||
|
}, get_http_data(Result3)),
|
||||||
|
|
||||||
{ok, _} = request_http_rest_update(<<"test_username">>, <<"new_password">>, error_format),
|
{ok, Result4} = request_http_rest_list(["auth_clientid"]),
|
||||||
{ok, _} = request_http_rest_update(<<"error_username">>, <<"new_password">>, false),
|
?assertEqual(3, length(get_http_data(Result4))),
|
||||||
|
|
||||||
{ok, _} = request_http_rest_update(<<"test_username">>, <<"new_password">>, false),
|
{ok, _} = request_http_rest_delete(["auth_clientid/" ++ binary_to_list(?CLIENTID)]),
|
||||||
{ok, Result4} = request_http_rest_lookup(<<"test_username">>),
|
{ok, Result5} = request_http_rest_lookup(["auth_clientid/" ++ binary_to_list(?CLIENTID)]),
|
||||||
#{<<"login">> := <<"test_username">>, <<"is_superuser">> := false} = get_http_data(Result4),
|
?assertMatch(#{}, get_http_data(Result5)).
|
||||||
|
|
||||||
User1 = #{username => <<"test_username">>,
|
|
||||||
password => <<"new_password">>,
|
|
||||||
zone => external},
|
|
||||||
|
|
||||||
{ok, #{is_superuser := false,
|
|
||||||
auth_result := success,
|
|
||||||
anonymous := false}} = emqx_access_control:authenticate(User1),
|
|
||||||
|
|
||||||
{ok, _} = request_http_rest_delete(<<"test_username">>),
|
|
||||||
{ok, #{auth_result := success,
|
|
||||||
anonymous := true }} = emqx_access_control:authenticate(User1).
|
|
||||||
|
|
||||||
t_run_command(_) ->
|
|
||||||
clean_all_users(),
|
|
||||||
?assertEqual(ok, emqx_ctl:run_command(["mqtt-user", "add", "TestUser", "Password", false])),
|
|
||||||
?assertMatch([{emqx_user, <<"TestUser">>, _, false}], emqx_auth_mnesia_cli:lookup_user(<<"TestUser">>)),
|
|
||||||
|
|
||||||
?assertEqual(ok, emqx_ctl:run_command(["mqtt-user", "update", "TestUser", "NewPassword", true])),
|
|
||||||
?assertMatch([{emqx_user, <<"TestUser">>, _, true}], emqx_auth_mnesia_cli:lookup_user(<<"TestUser">>)),
|
|
||||||
|
|
||||||
?assertEqual(ok, emqx_ctl:run_command(["mqtt-user", "del", "TestUser"])),
|
|
||||||
?assertMatch([], emqx_auth_mnesia_cli:lookup_user(<<"TestUser">>)),
|
|
||||||
|
|
||||||
?assertEqual(ok, emqx_ctl:run_command(["mqtt-user", "show", "TestUser"])),
|
|
||||||
?assertEqual(ok, emqx_ctl:run_command(["mqtt-user", "list"])),
|
|
||||||
?assertEqual(ok, emqx_ctl:run_command(["mqtt-user"])).
|
|
||||||
|
|
||||||
t_cli(_) ->
|
|
||||||
meck:new(emqx_ctl, [non_strict, passthrough]),
|
|
||||||
meck:expect(emqx_ctl, print, fun(Arg) -> emqx_ctl:format(Arg) end),
|
|
||||||
meck:expect(emqx_ctl, print, fun(Msg, Arg) -> emqx_ctl:format(Msg, Arg) end),
|
|
||||||
meck:expect(emqx_ctl, usage, fun(Usages) -> emqx_ctl:format_usage(Usages) end),
|
|
||||||
meck:expect(emqx_ctl, usage, fun(Cmd, Descr) -> emqx_ctl:format_usage(Cmd, Descr) end),
|
|
||||||
|
|
||||||
|
t_username_rest_api(_Config) ->
|
||||||
clean_all_users(),
|
clean_all_users(),
|
||||||
|
|
||||||
?assertMatch({match, _}, re:run(emqx_auth_mnesia_cli:auth_cli(["add", "TestUser", "Password", true]), "ok")),
|
{ok, Result1} = request_http_rest_list(["auth_username"]),
|
||||||
?assertMatch({match, _}, re:run(emqx_auth_mnesia_cli:auth_cli(["add", "TestUser", "Password", true]), "Error")),
|
[] = get_http_data(Result1),
|
||||||
|
|
||||||
?assertMatch({match, _}, re:run(emqx_auth_mnesia_cli:auth_cli(["update", "NoExisted", "Password", false]), "Error")),
|
Params1 = #{<<"username">> => ?USERNAME, <<"password">> => ?PASSWORD},
|
||||||
?assertMatch({match, _}, re:run(emqx_auth_mnesia_cli:auth_cli(["update", "TestUser", "Password", false]), "ok")),
|
{ok, _} = request_http_rest_add(["auth_username"], Params1),
|
||||||
|
|
||||||
?assertMatch(["User(login = <<\"TestUser\">> is_super = false)\n"], emqx_auth_mnesia_cli:auth_cli(["show", "TestUser"])),
|
Params2 = #{<<"username">> => ?USERNAME, <<"password">> => ?NPASSWORD},
|
||||||
?assertMatch(["User(login = <<\"TestUser\">>)\n"], emqx_auth_mnesia_cli:auth_cli(["list"])),
|
{ok, _} = request_http_rest_update(["auth_username/" ++ binary_to_list(?USERNAME)], Params2),
|
||||||
|
|
||||||
?assertMatch({match, _}, re:run(emqx_auth_mnesia_cli:auth_cli(["del", "TestUser"]), "ok")),
|
{ok, Result2} = request_http_rest_lookup(["auth_username/" ++ binary_to_list(?USERNAME)]),
|
||||||
?assertMatch([], emqx_auth_mnesia_cli:auth_cli(["show", "TestUser"])),
|
?assertMatch(#{<<"username">> := ?USERNAME}, get_http_data(Result2)),
|
||||||
?assertMatch([], emqx_auth_mnesia_cli:auth_cli(["list"])),
|
|
||||||
|
|
||||||
?assertMatch({match, _}, re:run(emqx_auth_mnesia_cli:auth_cli([]), "mqtt-user")),
|
Params3 = [ #{<<"username">> => ?USERNAME, <<"password">> => ?PASSWORD}
|
||||||
|
, #{<<"username">> => <<"username1">>, <<"password">> => ?PASSWORD}
|
||||||
|
, #{<<"username">> => <<"username2">>, <<"password">> => ?PASSWORD}
|
||||||
|
],
|
||||||
|
{ok, Result3} = request_http_rest_add(["auth_username"], Params3),
|
||||||
|
?assertMatch(#{ ?USERNAME := <<"{error,existed}">>
|
||||||
|
, <<"username1">> := <<"ok">>
|
||||||
|
, <<"username2">> := <<"ok">>
|
||||||
|
}, get_http_data(Result3)),
|
||||||
|
|
||||||
meck:unload(emqx_ctl).
|
{ok, Result4} = request_http_rest_list(["auth_username"]),
|
||||||
|
?assertEqual(3, length(get_http_data(Result4))),
|
||||||
|
|
||||||
|
{ok, _} = request_http_rest_delete(["auth_username/" ++ binary_to_list(?USERNAME)]),
|
||||||
|
{ok, Result5} = request_http_rest_lookup(["auth_username/" ++ binary_to_list(?USERNAME)]),
|
||||||
|
?assertMatch(#{}, get_http_data(Result5)).
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
%% Helpers
|
%% Helpers
|
||||||
|
@ -231,18 +257,17 @@ clean_all_users() ->
|
||||||
%% HTTP Request
|
%% HTTP Request
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
request_http_rest_list() ->
|
request_http_rest_list(Path) ->
|
||||||
request_api(get, uri(), default_auth_header()).
|
request_api(get, uri(Path), default_auth_header()).
|
||||||
|
|
||||||
request_http_rest_lookup(Login) ->
|
request_http_rest_lookup(Path) ->
|
||||||
request_api(get, uri([Login]), default_auth_header()).
|
request_api(get, uri([Path]), default_auth_header()).
|
||||||
|
|
||||||
request_http_rest_add(Params) ->
|
request_http_rest_add(Path, Params) ->
|
||||||
request_api(post, uri(), [], default_auth_header(), Params).
|
request_api(post, uri(Path), [], default_auth_header(), Params).
|
||||||
|
|
||||||
request_http_rest_update(Login, Password, IsSuperuser) ->
|
request_http_rest_update(Path, Params) ->
|
||||||
Params = #{<<"password">> => Password, <<"is_superuser">> => IsSuperuser},
|
request_api(put, uri([Path]), [], default_auth_header(), Params).
|
||||||
request_api(put, uri([Login]), [], default_auth_header(), Params).
|
|
||||||
|
|
||||||
request_http_rest_delete(Login) ->
|
request_http_rest_delete(Login) ->
|
||||||
request_api(delete, uri([Login]), default_auth_header()).
|
request_api(delete, uri([Login]), default_auth_header()).
|
||||||
|
@ -250,7 +275,7 @@ request_http_rest_delete(Login) ->
|
||||||
uri() -> uri([]).
|
uri() -> uri([]).
|
||||||
uri(Parts) when is_list(Parts) ->
|
uri(Parts) when is_list(Parts) ->
|
||||||
NParts = [b2l(E) || E <- Parts],
|
NParts = [b2l(E) || E <- Parts],
|
||||||
?HOST ++ filename:join([?BASE_PATH, ?API_VERSION, "mqtt_user"| NParts]).
|
?HOST ++ filename:join([?BASE_PATH, ?API_VERSION | NParts]).
|
||||||
|
|
||||||
%% @private
|
%% @private
|
||||||
b2l(B) when is_binary(B) ->
|
b2l(B) when is_binary(B) ->
|
||||||
|
|
|
@ -1,111 +0,0 @@
|
||||||
emqx_auth_username
|
|
||||||
==================
|
|
||||||
|
|
||||||
EMQ X Authentication with Username and Password
|
|
||||||
|
|
||||||
Build
|
|
||||||
-----
|
|
||||||
|
|
||||||
```
|
|
||||||
make && make tests
|
|
||||||
```
|
|
||||||
|
|
||||||
Configuration
|
|
||||||
-------------
|
|
||||||
|
|
||||||
etc/emqx_auth_username.conf:
|
|
||||||
|
|
||||||
```
|
|
||||||
## Password hash.
|
|
||||||
##
|
|
||||||
## Value: plain | md5 | sha | sha256
|
|
||||||
auth.user.password_hash = sha256
|
|
||||||
```
|
|
||||||
|
|
||||||
[REST API](https://developer.emqx.io/docs/emq/v3/en/rest.html)
|
|
||||||
------------
|
|
||||||
|
|
||||||
List all usernames
|
|
||||||
```
|
|
||||||
# Request
|
|
||||||
GET api/v4/auth_username
|
|
||||||
|
|
||||||
# Response
|
|
||||||
{
|
|
||||||
"code": 0,
|
|
||||||
"data": ["username1"]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Add a username:
|
|
||||||
```
|
|
||||||
# Request
|
|
||||||
POST api/v4/auth_username
|
|
||||||
{
|
|
||||||
"username": "some_name",
|
|
||||||
"password": "password"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Response
|
|
||||||
{
|
|
||||||
"code": 0
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Update password for a username:
|
|
||||||
```
|
|
||||||
# Request
|
|
||||||
PUT api/v4/auth_username/$NAME
|
|
||||||
{
|
|
||||||
"password": "password"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Response
|
|
||||||
{
|
|
||||||
"code", 0
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Lookup a username info:
|
|
||||||
```
|
|
||||||
# Request
|
|
||||||
GET api/v4/auth_username/$NAME
|
|
||||||
|
|
||||||
# Response
|
|
||||||
{
|
|
||||||
"code": 0,
|
|
||||||
"data": {
|
|
||||||
"username": "some_username",
|
|
||||||
"password": "hashed_password"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Delete a username:
|
|
||||||
```
|
|
||||||
# Request
|
|
||||||
DELETE api/v4/auth_username/$NAME
|
|
||||||
|
|
||||||
# Response
|
|
||||||
{
|
|
||||||
"code": 0
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Load the Plugin
|
|
||||||
---------------
|
|
||||||
|
|
||||||
```
|
|
||||||
./bin/emqx_ctl plugins load emqx_auth_username
|
|
||||||
```
|
|
||||||
|
|
||||||
License
|
|
||||||
-------
|
|
||||||
|
|
||||||
Apache License Version 2.0
|
|
||||||
|
|
||||||
Author
|
|
||||||
------
|
|
||||||
|
|
||||||
EMQ X Team.
|
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
Upgrade test cases for 3.0
|
|
|
@ -1,14 +0,0 @@
|
||||||
-define(APP, emqx_auth_username).
|
|
||||||
|
|
||||||
-record(auth_metrics, {
|
|
||||||
success = 'client.auth.success',
|
|
||||||
failure = 'client.auth.failure',
|
|
||||||
ignore = 'client.auth.ignore'
|
|
||||||
}).
|
|
||||||
|
|
||||||
-define(METRICS(Type), tl(tuple_to_list(#Type{}))).
|
|
||||||
-define(METRICS(Type, K), #Type{}#Type.K).
|
|
||||||
|
|
||||||
-define(AUTH_METRICS, ?METRICS(auth_metrics)).
|
|
||||||
-define(AUTH_METRICS(K), ?METRICS(auth_metrics, K)).
|
|
||||||
|
|
|
@ -1,26 +0,0 @@
|
||||||
%%-*- mode: erlang -*-
|
|
||||||
%% emqx_auth_username config mapping
|
|
||||||
|
|
||||||
{mapping, "auth.user.password_hash", "emqx_auth_username.password_hash", [
|
|
||||||
{default, sha256},
|
|
||||||
{datatype, {enum, [plain, md5, sha, sha256]}}
|
|
||||||
]}.
|
|
||||||
|
|
||||||
{mapping, "auth.user.$id.username", "emqx_auth_username.userlist", [
|
|
||||||
{datatype, string}
|
|
||||||
]}.
|
|
||||||
|
|
||||||
{mapping, "auth.user.$id.password", "emqx_auth_username.userlist", [
|
|
||||||
{datatype, string}
|
|
||||||
]}.
|
|
||||||
|
|
||||||
{translation, "emqx_auth_username.userlist", fun(Conf) ->
|
|
||||||
Userlist = cuttlefish_variable:filter_by_prefix("auth.user", Conf),
|
|
||||||
lists:foldl(
|
|
||||||
fun({["auth", "user", Id, "username"], Username}, AccIn) ->
|
|
||||||
[{Username, cuttlefish:conf_get("auth.user." ++ Id ++ ".password", Conf)} | AccIn];
|
|
||||||
(_, AccIn) ->
|
|
||||||
AccIn
|
|
||||||
end, [], Userlist)
|
|
||||||
end}.
|
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
{deps, []}.
|
|
|
@ -1,14 +0,0 @@
|
||||||
{application, emqx_auth_username,
|
|
||||||
[{description, "EMQ X Authentication with Username and Password"},
|
|
||||||
{vsn, "5.0.0"}, % strict semver, bump manually!
|
|
||||||
{modules, []},
|
|
||||||
{registered, []},
|
|
||||||
{applications, [kernel,stdlib,emqx_libs]},
|
|
||||||
{mod, {emqx_auth_username_app,[]}},
|
|
||||||
{env, []},
|
|
||||||
{licenses, ["Apache-2.0"]},
|
|
||||||
{maintainers, ["EMQ X Team <contact@emqx.io>"]},
|
|
||||||
{links, [{"Homepage", "https://emqx.io/"},
|
|
||||||
{"Github", "https://github.com/emqx/emqx-auth-username"}
|
|
||||||
]}
|
|
||||||
]}.
|
|
|
@ -1,173 +0,0 @@
|
||||||
%%--------------------------------------------------------------------
|
|
||||||
%% Copyright (c) 2020 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_auth_username).
|
|
||||||
|
|
||||||
-include("emqx_auth_username.hrl").
|
|
||||||
-include_lib("emqx_libs/include/emqx.hrl").
|
|
||||||
|
|
||||||
%% CLI callbacks
|
|
||||||
-export([cli/1]).
|
|
||||||
|
|
||||||
%% APIs
|
|
||||||
-export([ add_user/2
|
|
||||||
, update_password/2
|
|
||||||
, remove_user/1
|
|
||||||
, lookup_user/1
|
|
||||||
, all_users/0
|
|
||||||
]).
|
|
||||||
|
|
||||||
-export([unwrap_salt/1]).
|
|
||||||
|
|
||||||
%% Auth callbacks
|
|
||||||
-export([ init/1
|
|
||||||
, register_metrics/0
|
|
||||||
, check/3
|
|
||||||
, description/0
|
|
||||||
]).
|
|
||||||
|
|
||||||
-define(TAB, ?MODULE).
|
|
||||||
|
|
||||||
-record(?TAB, {username, password}).
|
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
|
||||||
%% CLI
|
|
||||||
%%--------------------------------------------------------------------
|
|
||||||
|
|
||||||
cli(["list"]) ->
|
|
||||||
Usernames = mnesia:dirty_all_keys(?TAB),
|
|
||||||
[emqx_ctl:print("~s~n", [Username]) || Username <- Usernames];
|
|
||||||
|
|
||||||
cli(["add", Username, Password]) ->
|
|
||||||
Ok = add_user(iolist_to_binary(Username), iolist_to_binary(Password)),
|
|
||||||
emqx_ctl:print("~p~n", [Ok]);
|
|
||||||
|
|
||||||
cli(["update", Username, NewPassword]) ->
|
|
||||||
Ok = update_password(iolist_to_binary(Username), iolist_to_binary(NewPassword)),
|
|
||||||
emqx_ctl:print("~p~n", [Ok]);
|
|
||||||
|
|
||||||
cli(["del", Username]) ->
|
|
||||||
emqx_ctl:print("~p~n", [remove_user(iolist_to_binary(Username))]);
|
|
||||||
|
|
||||||
cli(_) ->
|
|
||||||
emqx_ctl:usage([{"users list", "List users"},
|
|
||||||
{"users add <Username> <Password>", "Add User"},
|
|
||||||
{"users update <Username> <NewPassword>", "Update User"},
|
|
||||||
{"users del <Username>", "Delete User"}]).
|
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
|
||||||
%% API
|
|
||||||
%%--------------------------------------------------------------------
|
|
||||||
|
|
||||||
%% @doc Add User
|
|
||||||
-spec(add_user(binary(), binary()) -> ok | {error, any()}).
|
|
||||||
add_user(Username, Password) ->
|
|
||||||
User = #?TAB{username = Username, password = encrypted_data(Password)},
|
|
||||||
ret(mnesia:transaction(fun insert_user/1, [User])).
|
|
||||||
|
|
||||||
insert_user(User = #?TAB{username = Username}) ->
|
|
||||||
case mnesia:read(?TAB, Username) of
|
|
||||||
[] -> mnesia:write(User);
|
|
||||||
[_|_] -> mnesia:abort(existed)
|
|
||||||
end.
|
|
||||||
|
|
||||||
%% @doc Update User
|
|
||||||
-spec(update_password(binary(), binary()) -> ok | {error, any()}).
|
|
||||||
update_password(Username, NewPassword) ->
|
|
||||||
User = #?TAB{username = Username, password = encrypted_data(NewPassword)},
|
|
||||||
ret(mnesia:transaction(fun do_update_password/1, [User])).
|
|
||||||
|
|
||||||
do_update_password(User = #?TAB{username = Username}) ->
|
|
||||||
case mnesia:read(?TAB, Username) of
|
|
||||||
[_|_] -> mnesia:write(User);
|
|
||||||
[] -> mnesia:abort(noexisted)
|
|
||||||
end.
|
|
||||||
|
|
||||||
%% @doc Lookup user by username
|
|
||||||
-spec(lookup_user(binary()) -> list()).
|
|
||||||
lookup_user(Username) ->
|
|
||||||
mnesia:dirty_read(?TAB, Username).
|
|
||||||
|
|
||||||
%% @doc Remove user
|
|
||||||
-spec(remove_user(binary()) -> ok | {error, any()}).
|
|
||||||
remove_user(Username) ->
|
|
||||||
ret(mnesia:transaction(fun mnesia:delete/1, [{?TAB, Username}])).
|
|
||||||
|
|
||||||
ret({atomic, ok}) -> ok;
|
|
||||||
ret({aborted, Error}) -> {error, Error}.
|
|
||||||
|
|
||||||
%% @doc All usernames
|
|
||||||
-spec(all_users() -> list()).
|
|
||||||
all_users() -> mnesia:dirty_all_keys(?TAB).
|
|
||||||
|
|
||||||
unwrap_salt(<<_Salt:4/binary, HashPasswd/binary>>) ->
|
|
||||||
HashPasswd.
|
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
|
||||||
%% Auth callbacks
|
|
||||||
%%--------------------------------------------------------------------
|
|
||||||
|
|
||||||
init(DefaultUsers) ->
|
|
||||||
ok = ekka_mnesia:create_table(?TAB, [
|
|
||||||
{disc_copies, [node()]},
|
|
||||||
{attributes, record_info(fields, ?TAB)},
|
|
||||||
{storage_properties, [{ets, [{read_concurrency, true}]}]}]),
|
|
||||||
ok = lists:foreach(fun add_default_user/1, DefaultUsers),
|
|
||||||
ok = ekka_mnesia:copy_table(?TAB, disc_copies).
|
|
||||||
|
|
||||||
%% @private
|
|
||||||
add_default_user({Username, Password}) ->
|
|
||||||
add_user(iolist_to_binary(Username), iolist_to_binary(Password)).
|
|
||||||
|
|
||||||
-spec(register_metrics() -> ok).
|
|
||||||
register_metrics() ->
|
|
||||||
lists:foreach(fun emqx_metrics:ensure/1, ?AUTH_METRICS).
|
|
||||||
|
|
||||||
check(#{username := Username, password := Password}, AuthResult, #{hash_type := HashType}) ->
|
|
||||||
case mnesia:dirty_read(?TAB, Username) of
|
|
||||||
[] -> emqx_metrics:inc(?AUTH_METRICS(ignore));
|
|
||||||
[#?TAB{password = <<Salt:4/binary, Hash/binary>>}] ->
|
|
||||||
case Hash =:= hash(Password, Salt, HashType) of
|
|
||||||
true ->
|
|
||||||
ok = emqx_metrics:inc(?AUTH_METRICS(success)),
|
|
||||||
{stop, AuthResult#{auth_result => success, anonymous => false}};
|
|
||||||
false ->
|
|
||||||
ok = emqx_metrics:inc(?AUTH_METRICS(failure)),
|
|
||||||
{stop, AuthResult#{auth_result => not_authorized, anonymous => false}}
|
|
||||||
end
|
|
||||||
end.
|
|
||||||
|
|
||||||
description() ->
|
|
||||||
"Username password Authentication Module".
|
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
|
||||||
%% Internal functions
|
|
||||||
%%--------------------------------------------------------------------
|
|
||||||
|
|
||||||
encrypted_data(Password) ->
|
|
||||||
HashType = application:get_env(emqx_auth_username, password_hash, sha256),
|
|
||||||
SaltBin = salt(),
|
|
||||||
<<SaltBin/binary, (hash(Password, SaltBin, HashType))/binary>>.
|
|
||||||
|
|
||||||
hash(undefined, SaltBin, HashType) ->
|
|
||||||
hash(<<>>, SaltBin, HashType);
|
|
||||||
hash(Password, SaltBin, HashType) ->
|
|
||||||
emqx_passwd:hash(HashType, <<SaltBin/binary, Password/binary>>).
|
|
||||||
|
|
||||||
salt() ->
|
|
||||||
rand:seed(exsplus, erlang:timestamp()),
|
|
||||||
Salt = rand:uniform(16#ffffffff), <<Salt:32>>.
|
|
||||||
|
|
|
@ -1,123 +0,0 @@
|
||||||
%%--------------------------------------------------------------------
|
|
||||||
%% Copyright (c) 2020 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_auth_username_api).
|
|
||||||
|
|
||||||
-include("emqx_auth_username.hrl").
|
|
||||||
|
|
||||||
-import(proplists, [get_value/2]).
|
|
||||||
|
|
||||||
-import(minirest, [return/0, return/1]).
|
|
||||||
|
|
||||||
-rest_api(#{name => list_username,
|
|
||||||
method => 'GET',
|
|
||||||
path => "/auth_username",
|
|
||||||
func => list,
|
|
||||||
descr => "List available username in the cluster"
|
|
||||||
}).
|
|
||||||
|
|
||||||
-rest_api(#{name => lookup_username,
|
|
||||||
method => 'GET',
|
|
||||||
path => "/auth_username/:bin:username",
|
|
||||||
func => lookup,
|
|
||||||
descr => "Lookup username in the cluster"
|
|
||||||
}).
|
|
||||||
|
|
||||||
-rest_api(#{name => add_username,
|
|
||||||
method => 'POST',
|
|
||||||
path => "/auth_username",
|
|
||||||
func => add,
|
|
||||||
descr => "Add username in the cluster"
|
|
||||||
}).
|
|
||||||
|
|
||||||
-rest_api(#{name => update_username,
|
|
||||||
method => 'PUT',
|
|
||||||
path => "/auth_username/:bin:username",
|
|
||||||
func => update,
|
|
||||||
descr => "Update username in the cluster"
|
|
||||||
}).
|
|
||||||
|
|
||||||
-rest_api(#{name => delete_username,
|
|
||||||
method => 'DELETE',
|
|
||||||
path => "/auth_username/:bin:username",
|
|
||||||
func => delete,
|
|
||||||
descr => "Delete username in the cluster"
|
|
||||||
}).
|
|
||||||
|
|
||||||
-export([ list/2
|
|
||||||
, lookup/2
|
|
||||||
, add/2
|
|
||||||
, update/2
|
|
||||||
, delete/2
|
|
||||||
]).
|
|
||||||
|
|
||||||
list(_Bindings, _Params) ->
|
|
||||||
return({ok, emqx_auth_username:all_users()}).
|
|
||||||
|
|
||||||
lookup(#{username := Username}, _Params) ->
|
|
||||||
return({ok, format(emqx_auth_username:lookup_user(Username))}).
|
|
||||||
|
|
||||||
add(_Bindings, Params) ->
|
|
||||||
Username = get_value(<<"username">>, Params),
|
|
||||||
Password = get_value(<<"password">>, Params),
|
|
||||||
case validate([username, password], [Username, Password]) of
|
|
||||||
ok ->
|
|
||||||
case emqx_auth_username:add_user(Username, Password) of
|
|
||||||
ok -> return();
|
|
||||||
Err -> return(Err)
|
|
||||||
end;
|
|
||||||
Err -> return(Err)
|
|
||||||
end.
|
|
||||||
|
|
||||||
update(#{username := Username}, Params) ->
|
|
||||||
Password = get_value(<<"password">>, Params),
|
|
||||||
case validate([password], [Password]) of
|
|
||||||
ok ->
|
|
||||||
case emqx_auth_username:update_password(Username, Password) of
|
|
||||||
ok -> return();
|
|
||||||
Err -> return(Err)
|
|
||||||
end;
|
|
||||||
Err -> return(Err)
|
|
||||||
end.
|
|
||||||
|
|
||||||
delete(#{username := Username}, _) ->
|
|
||||||
ok = emqx_auth_username:remove_user(Username),
|
|
||||||
return().
|
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
|
||||||
%% Interval Funcs
|
|
||||||
%%------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
format([{?APP, Username, Password}]) ->
|
|
||||||
#{username => Username,
|
|
||||||
password => emqx_auth_username:unwrap_salt(Password)}.
|
|
||||||
|
|
||||||
validate([], []) ->
|
|
||||||
ok;
|
|
||||||
validate([K|Keys], [V|Values]) ->
|
|
||||||
case validation(K, V) of
|
|
||||||
false -> {error, K};
|
|
||||||
true -> validate(Keys, Values)
|
|
||||||
end.
|
|
||||||
|
|
||||||
validation(username, V) when is_binary(V)
|
|
||||||
andalso byte_size(V) > 0 ->
|
|
||||||
true;
|
|
||||||
validation(password, V) when is_binary(V)
|
|
||||||
andalso byte_size(V) > 0 ->
|
|
||||||
true;
|
|
||||||
validation(_, _) ->
|
|
||||||
false.
|
|
|
@ -1,49 +0,0 @@
|
||||||
%%--------------------------------------------------------------------
|
|
||||||
%% Copyright (c) 2020 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_auth_username_app).
|
|
||||||
|
|
||||||
-include("emqx_auth_username.hrl").
|
|
||||||
|
|
||||||
-behaviour(application).
|
|
||||||
-behaviour(supervisor).
|
|
||||||
|
|
||||||
-emqx_plugin(auth).
|
|
||||||
|
|
||||||
-export([ start/2
|
|
||||||
, stop/1
|
|
||||||
]).
|
|
||||||
-export([init/1]).
|
|
||||||
|
|
||||||
start(_Type, _Args) ->
|
|
||||||
emqx_ctl:register_command(users, {?APP, cli}, []),
|
|
||||||
ok = emqx_auth_username:register_metrics(),
|
|
||||||
HashType = application:get_env(?APP, password_hash, sha256),
|
|
||||||
Params = #{hash_type => HashType},
|
|
||||||
emqx:hook('client.authenticate', fun emqx_auth_username:check/3, [Params]),
|
|
||||||
DefaultUsers = application:get_env(?APP, userlist, []),
|
|
||||||
ok = emqx_auth_username:init(DefaultUsers),
|
|
||||||
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
|
|
||||||
|
|
||||||
stop(_State) ->
|
|
||||||
emqx:unhook('client.authenticate', fun emqx_auth_username:check/3),
|
|
||||||
emqx_ctl:unregister_command(users).
|
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
|
||||||
|
|
||||||
init([]) ->
|
|
||||||
{ok, { {one_for_all, 1, 10}, []} }.
|
|
||||||
|
|
|
@ -1,176 +0,0 @@
|
||||||
%% Copyright (c) 2020 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_auth_username_SUITE).
|
|
||||||
|
|
||||||
-compile(nowarn_export_all).
|
|
||||||
-compile(export_all).
|
|
||||||
|
|
||||||
-include_lib("emqx_libs/include/emqx.hrl").
|
|
||||||
|
|
||||||
-include_lib("eunit/include/eunit.hrl").
|
|
||||||
-include_lib("common_test/include/ct.hrl").
|
|
||||||
|
|
||||||
-define(TAB, emqx_auth_username).
|
|
||||||
|
|
||||||
all() ->
|
|
||||||
emqx_ct:all(?MODULE).
|
|
||||||
|
|
||||||
init_per_suite(Config) ->
|
|
||||||
emqx_ct_helpers:start_apps([emqx_auth_username], fun set_special_configs/1),
|
|
||||||
Config.
|
|
||||||
|
|
||||||
end_per_suite(_Config) ->
|
|
||||||
emqx_ct_helpers:stop_apps([emqx_auth_username]).
|
|
||||||
|
|
||||||
set_special_configs(emqx) ->
|
|
||||||
application:set_env(emqx, allow_anonymous, true),
|
|
||||||
application:set_env(emqx, enable_acl_cache, false),
|
|
||||||
LoadedPluginPath = filename:join(["test", "emqx_SUITE_data", "loaded_plugins"]),
|
|
||||||
application:set_env(emqx, plugins_loaded_file,
|
|
||||||
emqx_ct_helpers:deps_path(emqx, LoadedPluginPath));
|
|
||||||
|
|
||||||
set_special_configs(_App) ->
|
|
||||||
ok.
|
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
|
||||||
%% Testcases
|
|
||||||
%%------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
t_managing(_Config) ->
|
|
||||||
clean_all_users(),
|
|
||||||
|
|
||||||
ok = emqx_auth_username:add_user(<<"test_username">>, <<"password">>),
|
|
||||||
[{?TAB, <<"test_username">>, _HashedPass}] =
|
|
||||||
emqx_auth_username:lookup_user(<<"test_username">>),
|
|
||||||
User1 = #{username => <<"test_username">>,
|
|
||||||
password => <<"password">>,
|
|
||||||
zone => external},
|
|
||||||
|
|
||||||
{ok, #{auth_result := success,
|
|
||||||
anonymous := false}} = emqx_access_control:authenticate(User1),
|
|
||||||
|
|
||||||
ok = emqx_auth_username:remove_user(<<"test_username">>),
|
|
||||||
{ok, #{auth_result := success,
|
|
||||||
anonymous := true }} = emqx_access_control:authenticate(User1).
|
|
||||||
|
|
||||||
t_rest_api(_Config) ->
|
|
||||||
clean_all_users(),
|
|
||||||
|
|
||||||
Username = <<"username">>,
|
|
||||||
Password = <<"password">>,
|
|
||||||
Password1 = <<"password1">>,
|
|
||||||
User = #{username => Username, zone => external},
|
|
||||||
|
|
||||||
?assertEqual(return(),
|
|
||||||
emqx_auth_username_api:add(#{}, rest_params(Username, Password))),
|
|
||||||
?assertEqual(return({error, existed}),
|
|
||||||
emqx_auth_username_api:add(#{}, rest_params(Username, Password))),
|
|
||||||
?assertEqual(return([Username]),
|
|
||||||
emqx_auth_username_api:list(#{}, [])),
|
|
||||||
|
|
||||||
{ok, #{code := 0, data := Data}} =
|
|
||||||
emqx_auth_username_api:lookup(rest_binding(Username), []),
|
|
||||||
?assertEqual(true, match_password(maps:get(username, Data), Password)),
|
|
||||||
|
|
||||||
{ok, _} = emqx_access_control:authenticate(User#{password => Password}),
|
|
||||||
|
|
||||||
?assertEqual(return(),
|
|
||||||
emqx_auth_username_api:update(rest_binding(Username), rest_params(Password))),
|
|
||||||
?assertEqual(return({error, noexisted}),
|
|
||||||
emqx_auth_username_api:update(#{username => <<"another_user">>}, rest_params(<<"another_passwd">>))),
|
|
||||||
|
|
||||||
{error, _} = emqx_access_control:authenticate(User#{password => Password1}),
|
|
||||||
|
|
||||||
?assertEqual(return(),
|
|
||||||
emqx_auth_username_api:delete(rest_binding(Username), [])),
|
|
||||||
{ok, #{auth_result := success,
|
|
||||||
anonymous := true}} = emqx_access_control:authenticate(User#{password => Password}).
|
|
||||||
|
|
||||||
t_cli(_Config) ->
|
|
||||||
clean_all_users(),
|
|
||||||
|
|
||||||
emqx_auth_username:cli(["add", "username", "password"]),
|
|
||||||
?assertEqual(true, match_password(<<"username">>, <<"password">>)),
|
|
||||||
|
|
||||||
emqx_auth_username:cli(["update", "username", "newpassword"]),
|
|
||||||
?assertEqual(true, match_password(<<"username">>, <<"newpassword">>)),
|
|
||||||
|
|
||||||
emqx_auth_username:cli(["del", "username"]),
|
|
||||||
[] = emqx_auth_username:lookup_user(<<"username">>),
|
|
||||||
emqx_auth_username:cli(["add", "user1", "pass1"]),
|
|
||||||
emqx_auth_username:cli(["add", "user2", "pass2"]),
|
|
||||||
UserList = emqx_auth_username:cli(["list"]),
|
|
||||||
2 = length(UserList),
|
|
||||||
emqx_auth_username:cli(usage).
|
|
||||||
|
|
||||||
t_conf_not_override_existed(_) ->
|
|
||||||
clean_all_users(),
|
|
||||||
|
|
||||||
Username = <<"username">>,
|
|
||||||
Password = <<"password">>,
|
|
||||||
NPassword = <<"password1">>,
|
|
||||||
User = #{username => Username, zone => external},
|
|
||||||
|
|
||||||
application:stop(emqx_auth_username),
|
|
||||||
application:set_env(emqx_auth_username, userlist, [{Username, Password}]),
|
|
||||||
application:ensure_all_started(emqx_auth_username),
|
|
||||||
|
|
||||||
{ok, _} = emqx_access_control:authenticate(User#{password => Password}),
|
|
||||||
emqx_auth_username:cli(["update", Username, NPassword]),
|
|
||||||
|
|
||||||
{error, _} = emqx_access_control:authenticate(User#{password => Password}),
|
|
||||||
{ok, _} = emqx_access_control:authenticate(User#{password => NPassword}),
|
|
||||||
|
|
||||||
application:stop(emqx_auth_username),
|
|
||||||
application:ensure_all_started(emqx_auth_username),
|
|
||||||
{ok, _} = emqx_access_control:authenticate(User#{password => NPassword}),
|
|
||||||
|
|
||||||
?assertEqual(return(),
|
|
||||||
emqx_auth_username_api:update(rest_binding(Username), rest_params(Password))),
|
|
||||||
application:stop(emqx_auth_username),
|
|
||||||
application:ensure_all_started(emqx_auth_username),
|
|
||||||
{ok, _} = emqx_access_control:authenticate(User#{password => Password}).
|
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
|
||||||
%% Helpers
|
|
||||||
%%------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
clean_all_users() ->
|
|
||||||
[ mnesia:dirty_delete({emqx_auth_username, Username})
|
|
||||||
|| Username <- mnesia:dirty_all_keys(emqx_auth_username)].
|
|
||||||
|
|
||||||
match_password(Username, PlainPassword) ->
|
|
||||||
HashType = application:get_env(emqx_auth_username, password_hash, sha256),
|
|
||||||
[{?TAB, Username, <<Salt:4/binary, Hash/binary>>}] =
|
|
||||||
emqx_auth_username:lookup_user(Username),
|
|
||||||
Hash =:= emqx_passwd:hash(HashType, <<Salt/binary, PlainPassword/binary>>).
|
|
||||||
|
|
||||||
rest_params(Passwd) ->
|
|
||||||
[{<<"password">>, Passwd}].
|
|
||||||
|
|
||||||
rest_params(Username, Passwd) ->
|
|
||||||
[{<<"username">>, Username},
|
|
||||||
{<<"password">>, Passwd}].
|
|
||||||
|
|
||||||
rest_binding(Username) ->
|
|
||||||
#{username => Username}.
|
|
||||||
|
|
||||||
return() ->
|
|
||||||
{ok, #{code => 0}}.
|
|
||||||
return({error, Err}) ->
|
|
||||||
{ok, #{message => Err}};
|
|
||||||
return(Data) ->
|
|
||||||
{ok, #{code => 0, data => Data}}.
|
|
||||||
|
|
|
@ -119,8 +119,6 @@
|
||||||
, {emqx_sn, load}
|
, {emqx_sn, load}
|
||||||
, {emqx_coap, load}
|
, {emqx_coap, load}
|
||||||
, {emqx_stomp, load}
|
, {emqx_stomp, load}
|
||||||
, {emqx_auth_clientid, load}
|
|
||||||
, {emqx_auth_username, load}
|
|
||||||
, {emqx_auth_http, load}
|
, {emqx_auth_http, load}
|
||||||
, {emqx_auth_mysql, load}
|
, {emqx_auth_mysql, load}
|
||||||
, {emqx_auth_jwt, load}
|
, {emqx_auth_jwt, load}
|
||||||
|
|
Loading…
Reference in New Issue