build: delete needless auth plugins
This commit is contained in:
parent
2d1008ceaf
commit
c24f3688c4
|
@ -1,25 +0,0 @@
|
|||
.eunit
|
||||
deps
|
||||
*.o
|
||||
*.beam
|
||||
*.plt
|
||||
erl_crash.dump
|
||||
ebin
|
||||
rel/example_project
|
||||
.concrete/DEV_MODE
|
||||
.rebar
|
||||
.erlang.mk/
|
||||
emqx_auth_http.d
|
||||
data
|
||||
ct.cover.spec
|
||||
cover/
|
||||
ct.coverdata
|
||||
eunit.coverdata
|
||||
logs/
|
||||
erlang.mk
|
||||
_build/
|
||||
rebar.lock
|
||||
rebar3.crashdump
|
||||
etc/emqx_auth_http.conf.rendered
|
||||
.rebar3/
|
||||
*.swp
|
|
@ -1,100 +0,0 @@
|
|||
emqx_auth_http
|
||||
==============
|
||||
|
||||
EMQ X HTTP Auth/ACL Plugin
|
||||
|
||||
Build
|
||||
-----
|
||||
|
||||
```
|
||||
make && make tests
|
||||
```
|
||||
|
||||
Configure the Plugin
|
||||
--------------------
|
||||
|
||||
File: etc/emqx_auth_http.conf
|
||||
|
||||
```
|
||||
##--------------------------------------------------------------------
|
||||
## Authentication request.
|
||||
##
|
||||
## Variables:
|
||||
## - %u: username
|
||||
## - %c: clientid
|
||||
## - %a: ipaddress
|
||||
## - %r: protocol
|
||||
## - %P: password
|
||||
## - %C: common name of client TLS cert
|
||||
## - %d: subject of client TLS cert
|
||||
##
|
||||
## Value: URL
|
||||
auth.http.auth_req = http://127.0.0.1:8080/mqtt/auth
|
||||
## Value: post | get | put
|
||||
auth.http.auth_req.method = post
|
||||
## Value: Params
|
||||
auth.http.auth_req.params = clientid=%c,username=%u,password=%P
|
||||
|
||||
##--------------------------------------------------------------------
|
||||
## Superuser request.
|
||||
##
|
||||
## Variables:
|
||||
## - %u: username
|
||||
## - %c: clientid
|
||||
## - %a: ipaddress
|
||||
## - %r: protocol
|
||||
## - %P: password
|
||||
## - %C: common name of client TLS cert
|
||||
## - %d: subject of client TLS cert
|
||||
##
|
||||
## Value: URL
|
||||
auth.http.super_req = http://127.0.0.1:8080/mqtt/superuser
|
||||
## Value: post | get | put
|
||||
auth.http.super_req.method = post
|
||||
## Value: Params
|
||||
auth.http.super_req.params = clientid=%c,username=%u
|
||||
|
||||
##--------------------------------------------------------------------
|
||||
## ACL request.
|
||||
##
|
||||
## Variables:
|
||||
## - %A: 1 | 2, 1 = sub, 2 = pub
|
||||
## - %u: username
|
||||
## - %c: clientid
|
||||
## - %a: ipaddress
|
||||
## - %r: protocol
|
||||
## - %m: mountpoint
|
||||
## - %t: topic
|
||||
##
|
||||
## Value: URL
|
||||
auth.http.acl_req = http://127.0.0.1:8080/mqtt/acl
|
||||
## Value: post | get | put
|
||||
auth.http.acl_req.method = get
|
||||
## Value: Params
|
||||
auth.http.acl_req.params = access=%A,username=%u,clientid=%c,ipaddr=%a,topic=%t
|
||||
```
|
||||
|
||||
Load the Plugin
|
||||
---------------
|
||||
|
||||
```
|
||||
./bin/emqx_ctl plugins load emqx_auth_http
|
||||
```
|
||||
|
||||
HTTP API
|
||||
--------
|
||||
|
||||
200 if ok
|
||||
|
||||
4xx if unauthorized
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
Apache License Version 2.0
|
||||
|
||||
Author
|
||||
------
|
||||
|
||||
EMQ X Team.
|
||||
|
|
@ -1,172 +0,0 @@
|
|||
##--------------------------------------------------------------------
|
||||
## HTTP Auth/ACL Plugin
|
||||
##--------------------------------------------------------------------
|
||||
|
||||
## HTTP URL API path for Auth Request
|
||||
##
|
||||
## Value: URL
|
||||
##
|
||||
## Examples: http://127.0.0.1:80/mqtt/auth, https://[::1]:80/mqtt/auth
|
||||
auth.http.auth_req.url = "http://127.0.0.1:80/mqtt/auth"
|
||||
|
||||
## HTTP Request Method for Auth Request
|
||||
##
|
||||
## Value: post | get
|
||||
auth.http.auth_req.method = post
|
||||
|
||||
## HTTP Request Headers for Auth Request, Content-Type header is configured by default.
|
||||
## The possible values of the Content-Type header: application/x-www-form-urlencoded, application/json
|
||||
##
|
||||
## Examples: auth.http.auth_req.headers.accept = */*
|
||||
|
||||
auth.http.auth_req.headers.content_type = "application/x-www-form-urlencoded"
|
||||
|
||||
## Parameters used to construct the request body or query string parameters
|
||||
## When the request method is GET, these parameters will be converted into query string parameters
|
||||
## When the request method is POST, the final format is determined by content-type
|
||||
##
|
||||
## Available Variables:
|
||||
## - %u: username
|
||||
## - %c: clientid
|
||||
## - %a: ipaddress
|
||||
## - %r: protocol
|
||||
## - %P: password
|
||||
## - %p: sockport of server accepted
|
||||
## - %C: common name of client TLS cert
|
||||
## - %d: subject of client TLS cert
|
||||
##
|
||||
## Value: <K1>=<V1>,<K2>=<V2>,...
|
||||
auth.http.auth_req.params = "clientid=%c,username=%u,password=%P"
|
||||
|
||||
## HTTP URL API path for SuperUser Request
|
||||
##
|
||||
## Value: URL
|
||||
##
|
||||
## Examples: http://127.0.0.1:80/mqtt/superuser, https://[::1]:80/mqtt/superuser
|
||||
auth.http.super_req.url = "http://127.0.0.1:80/mqtt/superuser"
|
||||
|
||||
## HTTP Request Method for SuperUser Request
|
||||
##
|
||||
## Value: post | get
|
||||
auth.http.super_req.method = post
|
||||
|
||||
## HTTP Request Headers for SuperUser Request, Content-Type header is configured by default.
|
||||
## The possible values of the Content-Type header: application/x-www-form-urlencoded, application/json
|
||||
##
|
||||
## Examples: auth.http.super_req.headers.accept = */*
|
||||
auth.http.super_req.headers.content-type = "application/x-www-form-urlencoded"
|
||||
|
||||
## Parameters used to construct the request body or query string parameters
|
||||
## When the request method is GET, these parameters will be converted into query string parameters
|
||||
## When the request method is POST, the final format is determined by content-type
|
||||
##
|
||||
## Available Variables:
|
||||
## - %u: username
|
||||
## - %c: clientid
|
||||
## - %a: ipaddress
|
||||
## - %r: protocol
|
||||
## - %P: password
|
||||
## - %p: sockport of server accepted
|
||||
## - %C: common name of client TLS cert
|
||||
## - %d: subject of client TLS cert
|
||||
##
|
||||
## Value: <K1>=<V1>,<K2>=<V2>,...
|
||||
auth.http.super_req.params = "clientid=%c,username=%u"
|
||||
|
||||
## HTTP URL API path for ACL Request
|
||||
## Comment out this config to disable ACL checks
|
||||
##
|
||||
## Value: URL
|
||||
##
|
||||
## Examples: http://127.0.0.1:80/mqtt/acl, https://[::1]:80/mqtt/acl
|
||||
auth.http.acl_req.url = "http://127.0.0.1:80/mqtt/acl"
|
||||
|
||||
## HTTP Request Method for ACL Request
|
||||
##
|
||||
## Value: post | get
|
||||
auth.http.acl_req.method = post
|
||||
|
||||
## HTTP Request Headers for ACL Request, Content-Type header is configured by default.
|
||||
## The possible values of the Content-Type header: application/x-www-form-urlencoded, application/json
|
||||
##
|
||||
## Examples: auth.http.acl_req.headers.accept = */*
|
||||
auth.http.acl_req.headers.content-type = "application/x-www-form-urlencoded"
|
||||
|
||||
## Parameters used to construct the request body or query string parameters
|
||||
## When the request method is GET, these parameters will be converted into query string parameters
|
||||
## When the request method is POST, the final format is determined by content-type
|
||||
##
|
||||
## Available Variables:
|
||||
## - %A: access (1 - subscribe, 2 - publish)
|
||||
## - %u: username
|
||||
## - %c: clientid
|
||||
## - %a: ipaddress
|
||||
## - %r: protocol
|
||||
## - %P: password
|
||||
## - %p: sockport of server accepted
|
||||
## - %C: common name of client TLS cert
|
||||
## - %d: subject of client TLS cert
|
||||
## - %t: topic
|
||||
##
|
||||
## Value: <K1>=<V1>,<K2>=<V2>,...
|
||||
auth.http.acl_req.params = "access=%A,username=%u,clientid=%c,ipaddr=%a,topic=%t,mountpoint=%m"
|
||||
|
||||
## Time-out time for the request.
|
||||
##
|
||||
## Value: Duration
|
||||
## -h: hour, e.g. '2h' for 2 hours
|
||||
## -m: minute, e.g. '5m' for 5 minutes
|
||||
## -s: second, e.g. '30s' for 30 seconds
|
||||
##
|
||||
## Default: 5s
|
||||
auth.http.timeout = 5s
|
||||
|
||||
## Connection time-out time, used during the initial request,
|
||||
## when the client is connecting to the server.
|
||||
##
|
||||
## Value: Duration
|
||||
## -h: hour, e.g. '2h' for 2 hours
|
||||
## -m: minute, e.g. '5m' for 5 minutes
|
||||
## -s: second, e.g. '30s' for 30 seconds
|
||||
##
|
||||
## Default: 5s
|
||||
auth.http.connect_timeout = 5s
|
||||
|
||||
## Connection process pool size
|
||||
##
|
||||
## Value: Number
|
||||
auth.http.pool_size = 32
|
||||
|
||||
##------------------------------------------------------------------------------
|
||||
## SSL options
|
||||
|
||||
## Path to the file containing PEM-encoded CA certificates. The CA certificates
|
||||
## are used during server authentication and when building the client certificate chain.
|
||||
##
|
||||
## Value: File
|
||||
## auth.http.ssl.cacertfile = "{{ platform_etc_dir }}/certs/ca.pem"
|
||||
|
||||
## The path to a file containing the client's certificate.
|
||||
##
|
||||
## Value: File
|
||||
## auth.http.ssl.certfile = "{{ platform_etc_dir }}/certs/client-cert.pem"
|
||||
|
||||
## Path to a file containing the client's private PEM-encoded key.
|
||||
##
|
||||
## Value: File
|
||||
## auth.http.ssl.keyfile = "{{ platform_etc_dir }}/certs/client-key.pem"
|
||||
|
||||
## In mode verify_none the default behavior is to allow all x509-path
|
||||
## validation errors.
|
||||
##
|
||||
## Value: true | false
|
||||
## auth.http.ssl.verify = false
|
||||
|
||||
## If not specified, the server's names returned in server's certificate is validated against
|
||||
## what's provided `auth.http.auth_req.url` config's host part.
|
||||
## Setting to 'disable' will make EMQ X ignore unmatched server names.
|
||||
## If set with a host name, the server's names returned in server's certificate is validated
|
||||
## against this value.
|
||||
##
|
||||
## Value: String | disable
|
||||
## auth.http.ssl.server_name_indication = disable
|
|
@ -1,23 +0,0 @@
|
|||
|
||||
-define(APP, emqx_auth_http).
|
||||
|
||||
-record(auth_metrics, {
|
||||
success = 'client.auth.success',
|
||||
failure = 'client.auth.failure',
|
||||
ignore = 'client.auth.ignore'
|
||||
}).
|
||||
|
||||
-record(acl_metrics, {
|
||||
allow = 'client.acl.allow',
|
||||
deny = 'client.acl.deny',
|
||||
ignore = 'client.acl.ignore'
|
||||
}).
|
||||
|
||||
-define(METRICS(Type), tl(tuple_to_list(#Type{}))).
|
||||
-define(METRICS(Type, K), #Type{}#Type.K).
|
||||
|
||||
-define(AUTH_METRICS, ?METRICS(auth_metrics)).
|
||||
-define(AUTH_METRICS(K), ?METRICS(auth_metrics, K)).
|
||||
|
||||
-define(ACL_METRICS, ?METRICS(acl_metrics)).
|
||||
-define(ACL_METRICS(K), ?METRICS(acl_metrics, K)).
|
|
@ -1,131 +0,0 @@
|
|||
%%-*- mode: erlang -*-
|
||||
%% emqx_auth_http config mapping
|
||||
{mapping, "auth.http.auth_req.url", "emqx_auth_http.auth_req", [
|
||||
{datatype, string}
|
||||
]}.
|
||||
|
||||
{mapping, "auth.http.auth_req.method", "emqx_auth_http.auth_req", [
|
||||
{default, post},
|
||||
{datatype, {enum, [post, get]}}
|
||||
]}.
|
||||
|
||||
{mapping, "auth.http.auth_req.headers.$field", "emqx_auth_http.auth_req", [
|
||||
{datatype, string}
|
||||
]}.
|
||||
|
||||
{mapping, "auth.http.auth_req.params", "emqx_auth_http.auth_req", [
|
||||
{datatype, string}
|
||||
]}.
|
||||
|
||||
{translation, "emqx_auth_http.auth_req", fun(Conf) ->
|
||||
case cuttlefish:conf_get("auth.http.auth_req.url", Conf, undefined) of
|
||||
undefined -> cuttlefish:unset();
|
||||
Url ->
|
||||
Headers = cuttlefish_variable:filter_by_prefix("auth.http.auth_req.headers", Conf),
|
||||
Params = cuttlefish:conf_get("auth.http.auth_req.params", Conf),
|
||||
[{url, Url},
|
||||
{method, cuttlefish:conf_get("auth.http.auth_req.method", Conf)},
|
||||
{headers, [{K, V} || {[_, _, _, _, K], V} <- Headers]},
|
||||
{params, [list_to_tuple(string:tokens(S, "=")) || S <- string:tokens(Params, ",")]}]
|
||||
end
|
||||
end}.
|
||||
|
||||
{mapping, "auth.http.super_req.url", "emqx_auth_http.super_req", [
|
||||
{datatype, string}
|
||||
]}.
|
||||
|
||||
{mapping, "auth.http.super_req.method", "emqx_auth_http.super_req", [
|
||||
{default, post},
|
||||
{datatype, {enum, [post, get]}}
|
||||
]}.
|
||||
|
||||
{mapping, "auth.http.super_req.headers.$field", "emqx_auth_http.super_req", [
|
||||
{datatype, string}
|
||||
]}.
|
||||
|
||||
{mapping, "auth.http.super_req.params", "emqx_auth_http.super_req", [
|
||||
{datatype, string}
|
||||
]}.
|
||||
|
||||
{translation, "emqx_auth_http.super_req", fun(Conf) ->
|
||||
case cuttlefish:conf_get("auth.http.super_req.url", Conf, undefined) of
|
||||
undefined -> cuttlefish:unset();
|
||||
Url ->
|
||||
Headers = cuttlefish_variable:filter_by_prefix("auth.http.super_req.headers", Conf),
|
||||
Params = cuttlefish:conf_get("auth.http.super_req.params", Conf),
|
||||
[{url, Url},
|
||||
{method, cuttlefish:conf_get("auth.http.super_req.method", Conf)},
|
||||
{headers, [{K, V} || {[_, _, _, _, K], V} <- Headers]},
|
||||
{params, [list_to_tuple(string:tokens(S, "=")) || S <- string:tokens(Params, ",")]}]
|
||||
end
|
||||
end}.
|
||||
|
||||
%% @doc URL for ACL checks. Example: http://127.0.0.1:80/mqtt/acl
|
||||
%% ACL checks are disabled for this plugin if this config is
|
||||
%% commented out from the config file, or when the overriding
|
||||
%% environment variable is set to empty string.
|
||||
{mapping, "auth.http.acl_req.url", "emqx_auth_http.acl_req", [
|
||||
{datatype, string}
|
||||
]}.
|
||||
|
||||
{mapping, "auth.http.acl_req.method", "emqx_auth_http.acl_req", [
|
||||
{default, post},
|
||||
{datatype, {enum, [post, get]}}
|
||||
]}.
|
||||
|
||||
{mapping, "auth.http.acl_req.headers.$field", "emqx_auth_http.acl_req", [
|
||||
{datatype, string}
|
||||
]}.
|
||||
|
||||
{mapping, "auth.http.acl_req.params", "emqx_auth_http.acl_req", [
|
||||
{datatype, string}
|
||||
]}.
|
||||
|
||||
{translation, "emqx_auth_http.acl_req", fun(Conf) ->
|
||||
case cuttlefish:conf_get("auth.http.acl_req.url", Conf, undefined) of
|
||||
undefined -> cuttlefish:unset();
|
||||
Url ->
|
||||
Headers = cuttlefish_variable:filter_by_prefix("auth.http.acl_req.headers", Conf),
|
||||
Params = cuttlefish:conf_get("auth.http.acl_req.params", Conf),
|
||||
[{url, Url},
|
||||
{method, cuttlefish:conf_get("auth.http.acl_req.method", Conf)},
|
||||
{headers, [{K, V} || {[_, _, _, _, K], V} <- Headers]},
|
||||
{params, [list_to_tuple(string:tokens(S, "=")) || S <- string:tokens(Params, ",")]}]
|
||||
end
|
||||
end}.
|
||||
|
||||
{mapping, "auth.http.timeout", "emqx_auth_http.timeout", [
|
||||
{default, "5s"},
|
||||
{datatype, [integer, {duration, ms}]}
|
||||
]}.
|
||||
|
||||
{mapping, "auth.http.connect_timeout", "emqx_auth_http.connect_timeout", [
|
||||
{default, "5s"},
|
||||
{datatype, [integer, {duration, ms}]}
|
||||
]}.
|
||||
|
||||
{mapping, "auth.http.pool_size", "emqx_auth_http.pool_size", [
|
||||
{default, 8},
|
||||
{datatype, integer}
|
||||
]}.
|
||||
|
||||
{mapping, "auth.http.ssl.cacertfile", "emqx_auth_http.cacertfile", [
|
||||
{datatype, string}
|
||||
]}.
|
||||
|
||||
{mapping, "auth.http.ssl.certfile", "emqx_auth_http.certfile", [
|
||||
{datatype, string}
|
||||
]}.
|
||||
|
||||
{mapping, "auth.http.ssl.keyfile", "emqx_auth_http.keyfile", [
|
||||
{datatype, string}
|
||||
]}.
|
||||
|
||||
{mapping, "auth.http.ssl.verify", "emqx_auth_http.verify", [
|
||||
{default, false},
|
||||
{datatype, {enum, [true, false]}}
|
||||
]}.
|
||||
|
||||
{mapping, "auth.http.ssl.server_name_indication", "emqx_auth_http.server_name_indication", [
|
||||
{datatype, string}
|
||||
]}.
|
|
@ -1,26 +0,0 @@
|
|||
{deps, []}.
|
||||
|
||||
{edoc_opts, [{preprocess, true}]}.
|
||||
{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}.
|
||||
|
||||
{profiles,
|
||||
[{test,
|
||||
[{deps,
|
||||
[
|
||||
{emqtt, {git, "https://github.com/emqx/emqtt", {tag, "v1.2.2"}}}
|
||||
]}
|
||||
]}
|
||||
]}.
|
|
@ -1,88 +0,0 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_acl_http).
|
||||
|
||||
-include("emqx_auth_http.hrl").
|
||||
|
||||
-include_lib("emqx/include/emqx.hrl").
|
||||
-include_lib("emqx/include/logger.hrl").
|
||||
|
||||
-logger_header("[ACL http]").
|
||||
|
||||
-import(emqx_auth_http_cli,
|
||||
[ request/6
|
||||
, feedvar/2
|
||||
]).
|
||||
|
||||
%% ACL callbacks
|
||||
-export([ register_metrics/0
|
||||
, check_acl/5
|
||||
, description/0
|
||||
]).
|
||||
|
||||
-spec(register_metrics() -> ok).
|
||||
register_metrics() ->
|
||||
lists:foreach(fun emqx_metrics:ensure/1, ?ACL_METRICS).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% ACL callbacks
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
check_acl(ClientInfo, PubSub, Topic, AclResult, Params) ->
|
||||
return_with(fun inc_metrics/1,
|
||||
do_check_acl(ClientInfo, PubSub, Topic, AclResult, Params)).
|
||||
|
||||
do_check_acl(#{username := <<$$, _/binary>>}, _PubSub, _Topic, _AclResult, _Params) ->
|
||||
ok;
|
||||
do_check_acl(ClientInfo, PubSub, Topic, _AclResult, #{acl := ACLParams = #{path := Path}}) ->
|
||||
ClientInfo1 = ClientInfo#{access => access(PubSub), topic => Topic},
|
||||
case check_acl_request(ACLParams, ClientInfo1) of
|
||||
{ok, 200, <<"ignore">>} -> ok;
|
||||
{ok, 200, _Body} -> {stop, allow};
|
||||
{ok, _Code, _Body} -> {stop, deny};
|
||||
{error, Error} ->
|
||||
?LOG(error, "Request ACL path ~s, error: ~p", [Path, Error]),
|
||||
ok
|
||||
end.
|
||||
|
||||
description() -> "ACL with HTTP API".
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Internal functions
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
inc_metrics(ok) ->
|
||||
emqx_metrics:inc(?ACL_METRICS(ignore));
|
||||
inc_metrics({stop, allow}) ->
|
||||
emqx_metrics:inc(?ACL_METRICS(allow));
|
||||
inc_metrics({stop, deny}) ->
|
||||
emqx_metrics:inc(?ACL_METRICS(deny)).
|
||||
|
||||
return_with(Fun, Result) ->
|
||||
Fun(Result), Result.
|
||||
|
||||
check_acl_request(#{pool_name := PoolName,
|
||||
path := Path,
|
||||
method := Method,
|
||||
headers := Headers,
|
||||
params := Params,
|
||||
timeout := Timeout}, ClientInfo) ->
|
||||
request(PoolName, Method, Path, Headers, feedvar(Params, ClientInfo), Timeout).
|
||||
|
||||
access(subscribe) -> 1;
|
||||
access(publish) -> 2.
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
{application, emqx_auth_http,
|
||||
[{description, "EMQ X Authentication/ACL with HTTP API"},
|
||||
{vsn, "4.3.0"}, % strict semver, bump manually!
|
||||
{modules, []},
|
||||
{registered, [emqx_auth_http_sup]},
|
||||
{applications, [kernel,stdlib,ehttpc]},
|
||||
{mod, {emqx_auth_http_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-http"}
|
||||
]}
|
||||
]}.
|
|
@ -1,112 +0,0 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_auth_http).
|
||||
|
||||
-include("emqx_auth_http.hrl").
|
||||
|
||||
-include_lib("emqx/include/emqx.hrl").
|
||||
-include_lib("emqx/include/logger.hrl").
|
||||
-include_lib("emqx/include/types.hrl").
|
||||
|
||||
-logger_header("[Auth http]").
|
||||
|
||||
-import(emqx_auth_http_cli,
|
||||
[ request/6
|
||||
, feedvar/2
|
||||
]).
|
||||
|
||||
%% Callbacks
|
||||
-export([ register_metrics/0
|
||||
, check/3
|
||||
, description/0
|
||||
]).
|
||||
|
||||
-spec(register_metrics() -> ok).
|
||||
register_metrics() ->
|
||||
lists:foreach(fun emqx_metrics:ensure/1, ?AUTH_METRICS).
|
||||
|
||||
check(ClientInfo, AuthResult, #{auth := AuthParms = #{path := Path},
|
||||
super := SuperParams}) ->
|
||||
case authenticate(AuthParms, ClientInfo) of
|
||||
{ok, 200, <<"ignore">>} ->
|
||||
emqx_metrics:inc(?AUTH_METRICS(ignore)), ok;
|
||||
{ok, 200, Body} ->
|
||||
emqx_metrics:inc(?AUTH_METRICS(success)),
|
||||
IsSuperuser = is_superuser(SuperParams, ClientInfo),
|
||||
{stop, AuthResult#{is_superuser => IsSuperuser,
|
||||
auth_result => success,
|
||||
anonymous => false,
|
||||
mountpoint => mountpoint(Body, ClientInfo)}};
|
||||
{ok, Code, _Body} ->
|
||||
?LOG(error, "Deny connection from path: ~s, response http code: ~p",
|
||||
[Path, Code]),
|
||||
emqx_metrics:inc(?AUTH_METRICS(failure)),
|
||||
{stop, AuthResult#{auth_result => http_to_connack_error(Code),
|
||||
anonymous => false}};
|
||||
{error, Error} ->
|
||||
?LOG(error, "Request auth path: ~s, error: ~p", [Path, Error]),
|
||||
emqx_metrics:inc(?AUTH_METRICS(failure)),
|
||||
%%FIXME later: server_unavailable is not right.
|
||||
{stop, AuthResult#{auth_result => server_unavailable,
|
||||
anonymous => false}}
|
||||
end.
|
||||
|
||||
description() -> "Authentication by HTTP API".
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Requests
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
authenticate(#{pool_name := PoolName,
|
||||
path := Path,
|
||||
method := Method,
|
||||
headers := Headers,
|
||||
params := Params,
|
||||
timeout := Timeout}, ClientInfo) ->
|
||||
request(PoolName, Method, Path, Headers, feedvar(Params, ClientInfo), Timeout).
|
||||
|
||||
-spec(is_superuser(maybe(map()), emqx_types:client()) -> boolean()).
|
||||
is_superuser(undefined, _ClientInfo) ->
|
||||
false;
|
||||
is_superuser(#{pool_name := PoolName,
|
||||
path := Path,
|
||||
method := Method,
|
||||
headers := Headers,
|
||||
params := Params,
|
||||
timeout := Timeout}, ClientInfo) ->
|
||||
case request(PoolName, Method, Path, Headers, feedvar(Params, ClientInfo), Timeout) of
|
||||
{ok, 200, _Body} -> true;
|
||||
{ok, _Code, _Body} -> false;
|
||||
{error, Error} -> ?LOG(error, "Request superuser path ~s, error: ~p", [Path, Error]),
|
||||
false
|
||||
end.
|
||||
|
||||
mountpoint(Body, #{mountpoint := Mountpoint}) ->
|
||||
case emqx_json:safe_decode(Body, [return_maps]) of
|
||||
{error, _} -> Mountpoint;
|
||||
{ok, Json} when is_map(Json) ->
|
||||
maps:get(<<"mountpoint">>, Json, Mountpoint);
|
||||
{ok, _NotMap} -> Mountpoint
|
||||
end.
|
||||
|
||||
http_to_connack_error(400) -> bad_username_or_password;
|
||||
http_to_connack_error(401) -> bad_username_or_password;
|
||||
http_to_connack_error(403) -> not_authorized;
|
||||
http_to_connack_error(429) -> banned;
|
||||
http_to_connack_error(503) -> server_unavailable;
|
||||
http_to_connack_error(504) -> server_busy;
|
||||
http_to_connack_error(_) -> server_unavailable.
|
|
@ -1,158 +0,0 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_auth_http_app).
|
||||
|
||||
-behaviour(application).
|
||||
|
||||
-emqx_plugin(auth).
|
||||
|
||||
-include("emqx_auth_http.hrl").
|
||||
|
||||
-export([ start/2
|
||||
, stop/1
|
||||
]).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Application Callbacks
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
start(_StartType, _StartArgs) ->
|
||||
{ok, Sup} = emqx_auth_http_sup:start_link(),
|
||||
translate_env(),
|
||||
load_hooks(),
|
||||
{ok, Sup}.
|
||||
|
||||
stop(_State) ->
|
||||
unload_hooks().
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Internel functions
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
translate_env() ->
|
||||
lists:foreach(fun translate_env/1, [auth_req, super_req, acl_req]).
|
||||
|
||||
translate_env(EnvName) ->
|
||||
case application:get_env(?APP, EnvName) of
|
||||
undefined -> ok;
|
||||
{ok, Req} ->
|
||||
{ok, PoolSize} = application:get_env(?APP, pool_size),
|
||||
{ok, ConnectTimeout} = application:get_env(?APP, connect_timeout),
|
||||
URL = proplists:get_value(url, Req),
|
||||
{ok, #{host := Host,
|
||||
path := Path0,
|
||||
port := Port,
|
||||
scheme := Scheme}} = emqx_http_lib:uri_parse(URL),
|
||||
Path = path(Path0),
|
||||
MoreOpts = case Scheme of
|
||||
http ->
|
||||
[{transport_opts, emqx_misc:ipv6_probe([])}];
|
||||
https ->
|
||||
CACertFile = application:get_env(?APP, cacertfile, undefined),
|
||||
CertFile = application:get_env(?APP, certfile, undefined),
|
||||
KeyFile = application:get_env(?APP, keyfile, undefined),
|
||||
Verify = case application:get_env(?APP, verify, fasle) of
|
||||
true -> verify_peer;
|
||||
false -> verify_none
|
||||
end,
|
||||
SNI = case application:get_env(?APP, server_name_indication, undefined) of
|
||||
"disable" -> disable;
|
||||
SNI0 -> SNI0
|
||||
end,
|
||||
TLSOpts = lists:filter(
|
||||
fun({_, V}) ->
|
||||
V =/= <<>> andalso V =/= undefined
|
||||
end, [{keyfile, KeyFile},
|
||||
{certfile, CertFile},
|
||||
{cacertfile, CACertFile},
|
||||
{verify, Verify},
|
||||
{server_name_indication, SNI}]),
|
||||
NTLSOpts = [ {versions, emqx_tls_lib:default_versions()}
|
||||
, {ciphers, emqx_tls_lib:default_ciphers()}
|
||||
| TLSOpts
|
||||
],
|
||||
[{transport, ssl}, {transport_opts, emqx_misc:ipv6_probe(NTLSOpts)}]
|
||||
end,
|
||||
PoolOpts = [{host, Host},
|
||||
{port, Port},
|
||||
{pool_size, PoolSize},
|
||||
{pool_type, random},
|
||||
{connect_timeout, ConnectTimeout},
|
||||
{retry, 5},
|
||||
{retry_timeout, 1000}] ++ MoreOpts,
|
||||
Method = proplists:get_value(method, Req),
|
||||
Headers = proplists:get_value(headers, Req),
|
||||
NHeaders = ensure_content_type_header(Method, emqx_http_lib:normalise_headers(Headers)),
|
||||
NReq = lists:keydelete(headers, 1, Req),
|
||||
{ok, Timeout} = application:get_env(?APP, timeout),
|
||||
application:set_env(?APP, EnvName, [{path, Path},
|
||||
{headers, NHeaders},
|
||||
{timeout, Timeout},
|
||||
{pool_name, list_to_atom("emqx_auth_http/" ++ atom_to_list(EnvName))},
|
||||
{pool_opts, PoolOpts} | NReq])
|
||||
end.
|
||||
|
||||
load_hooks() ->
|
||||
case application:get_env(?APP, auth_req) of
|
||||
undefined -> ok;
|
||||
{ok, AuthReq} ->
|
||||
ok = emqx_auth_http:register_metrics(),
|
||||
PoolOpts = proplists:get_value(pool_opts, AuthReq),
|
||||
PoolName = proplists:get_value(pool_name, AuthReq),
|
||||
{ok, _} = ehttpc_sup:start_pool(PoolName, PoolOpts),
|
||||
case application:get_env(?APP, super_req) of
|
||||
undefined ->
|
||||
emqx_hooks:put('client.authenticate', {emqx_auth_http, check, [#{auth => maps:from_list(AuthReq),
|
||||
super => undefined}]});
|
||||
{ok, SuperReq} ->
|
||||
PoolOpts1 = proplists:get_value(pool_opts, SuperReq),
|
||||
PoolName1 = proplists:get_value(pool_name, SuperReq),
|
||||
{ok, _} = ehttpc_sup:start_pool(PoolName1, PoolOpts1),
|
||||
emqx_hooks:put('client.authenticate', {emqx_auth_http, check, [#{auth => maps:from_list(AuthReq),
|
||||
super => maps:from_list(SuperReq)}]})
|
||||
end
|
||||
end,
|
||||
case application:get_env(?APP, acl_req) of
|
||||
undefined -> ok;
|
||||
{ok, ACLReq} ->
|
||||
ok = emqx_acl_http:register_metrics(),
|
||||
PoolOpts2 = proplists:get_value(pool_opts, ACLReq),
|
||||
PoolName2 = proplists:get_value(pool_name, ACLReq),
|
||||
{ok, _} = ehttpc_sup:start_pool(PoolName2, PoolOpts2),
|
||||
emqx_hooks:put('client.check_acl', {emqx_acl_http, check_acl, [#{acl => maps:from_list(ACLReq)}]})
|
||||
end,
|
||||
ok.
|
||||
|
||||
unload_hooks() ->
|
||||
emqx:unhook('client.authenticate', {emqx_auth_http, check}),
|
||||
emqx:unhook('client.check_acl', {emqx_acl_http, check_acl}),
|
||||
_ = ehttpc_sup:stop_pool('emqx_auth_http/auth_req'),
|
||||
_ = ehttpc_sup:stop_pool('emqx_auth_http/super_req'),
|
||||
_ = ehttpc_sup:stop_pool('emqx_auth_http/acl_req'),
|
||||
ok.
|
||||
|
||||
ensure_content_type_header(Method, Headers)
|
||||
when Method =:= post orelse Method =:= put ->
|
||||
Headers;
|
||||
ensure_content_type_header(_Method, Headers) ->
|
||||
lists:keydelete("content-type", 1, Headers).
|
||||
|
||||
path("") ->
|
||||
"/";
|
||||
path(Path) ->
|
||||
Path.
|
||||
|
|
@ -1,92 +0,0 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_auth_http_cli).
|
||||
|
||||
-include("emqx_auth_http.hrl").
|
||||
|
||||
-export([ request/6
|
||||
, feedvar/2
|
||||
, feedvar/3
|
||||
]).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% HTTP Request
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
request(PoolName, get, Path, Headers, Params, Timeout) ->
|
||||
NewPath = Path ++ "?" ++ binary_to_list(cow_qs:qs(bin_kw(Params))),
|
||||
reply(ehttpc:request(ehttpc_pool:pick_worker(PoolName), get, {NewPath, Headers}, Timeout));
|
||||
|
||||
request(PoolName, post, Path, Headers, Params, Timeout) ->
|
||||
Body = case proplists:get_value("content-type", Headers) of
|
||||
"application/x-www-form-urlencoded" ->
|
||||
cow_qs:qs(bin_kw(Params));
|
||||
"application/json" ->
|
||||
emqx_json:encode(bin_kw(Params))
|
||||
end,
|
||||
reply(ehttpc:request(ehttpc_pool:pick_worker(PoolName), post, {Path, Headers, Body}, Timeout)).
|
||||
|
||||
reply({ok, StatusCode, _Headers}) ->
|
||||
{ok, StatusCode, <<>>};
|
||||
reply({ok, StatusCode, _Headers, Body}) ->
|
||||
{ok, StatusCode, Body};
|
||||
reply({error, Reason}) ->
|
||||
{error, Reason}.
|
||||
|
||||
%% TODO: move this conversion to cuttlefish config and schema
|
||||
bin_kw(KeywordList) when is_list(KeywordList) ->
|
||||
[{bin(K), bin(V)} || {K, V} <- KeywordList].
|
||||
|
||||
bin(Atom) when is_atom(Atom) ->
|
||||
list_to_binary(atom_to_list(Atom));
|
||||
bin(Int) when is_integer(Int) ->
|
||||
integer_to_binary(Int);
|
||||
bin(Float) when is_float(Float) ->
|
||||
float_to_binary(Float, [{decimals, 12}, compact]);
|
||||
bin(List) when is_list(List)->
|
||||
list_to_binary(List);
|
||||
bin(Binary) when is_binary(Binary) ->
|
||||
Binary.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Feed Variables
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
feedvar(Params, ClientInfo = #{clientid := ClientId,
|
||||
protocol := Protocol,
|
||||
peerhost := Peerhost}) ->
|
||||
lists:map(fun({Param, "%u"}) -> {Param, maps:get(username, ClientInfo, null)};
|
||||
({Param, "%c"}) -> {Param, ClientId};
|
||||
({Param, "%r"}) -> {Param, Protocol};
|
||||
({Param, "%a"}) -> {Param, inet:ntoa(Peerhost)};
|
||||
({Param, "%P"}) -> {Param, maps:get(password, ClientInfo, null)};
|
||||
({Param, "%p"}) -> {Param, maps:get(sockport, ClientInfo, null)};
|
||||
({Param, "%C"}) -> {Param, maps:get(cn, ClientInfo, null)};
|
||||
({Param, "%d"}) -> {Param, maps:get(dn, ClientInfo, null)};
|
||||
({Param, "%A"}) -> {Param, maps:get(access, ClientInfo, null)};
|
||||
({Param, "%t"}) -> {Param, maps:get(topic, ClientInfo, null)};
|
||||
({Param, "%m"}) -> {Param, maps:get(mountpoint, ClientInfo, null)};
|
||||
({Param, Var}) -> {Param, Var}
|
||||
end, Params).
|
||||
|
||||
feedvar(Params, Var, Val) ->
|
||||
lists:map(fun({Param, Var0}) when Var0 == Var ->
|
||||
{Param, Val};
|
||||
({Param, Var0}) ->
|
||||
{Param, Var0}
|
||||
end, Params).
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_auth_http_sup).
|
||||
|
||||
-behaviour(supervisor).
|
||||
|
||||
-export([start_link/0]).
|
||||
|
||||
-export([init/1]).
|
||||
|
||||
start_link() ->
|
||||
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
|
||||
|
||||
init([]) ->
|
||||
{ok, {{one_for_all, 0, 1}, []}}.
|
|
@ -1,257 +0,0 @@
|
|||
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
|
||||
-module(emqx_auth_http_SUITE).
|
||||
|
||||
-compile(export_all).
|
||||
-compile(nowarn_export_all).
|
||||
|
||||
-include_lib("emqx/include/emqx.hrl").
|
||||
-include_lib("common_test/include/ct.hrl").
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
|
||||
-define(APP, emqx_auth_http).
|
||||
|
||||
-define(USER(ClientId, Username, Protocol, Peerhost, Zone),
|
||||
#{clientid => ClientId, username => Username, protocol => Protocol,
|
||||
peerhost => Peerhost, zone => Zone}).
|
||||
|
||||
-define(USER(ClientId, Username, Protocol, Peerhost, Zone, Mountpoint),
|
||||
#{clientid => ClientId, username => Username, protocol => Protocol,
|
||||
peerhost => Peerhost, zone => Zone, mountpoint => Mountpoint}).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Setups
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
all() ->
|
||||
[
|
||||
{group, http_inet},
|
||||
{group, http_inet6},
|
||||
{group, https_inet},
|
||||
{group, https_inet6},
|
||||
pub_sub_no_acl,
|
||||
no_hook_if_config_unset
|
||||
].
|
||||
|
||||
groups() ->
|
||||
Cases = emqx_ct:all(?MODULE),
|
||||
[{Name, Cases} || Name <- [http_inet, http_inet6, https_inet, https_inet6]].
|
||||
|
||||
init_per_group(GrpName, Cfg) ->
|
||||
[Scheme, Inet] = [list_to_atom(X) || X <- string:tokens(atom_to_list(GrpName), "_")],
|
||||
ok = setup(Scheme, Inet),
|
||||
Cfg.
|
||||
|
||||
end_per_group(_GrpName, _Cfg) ->
|
||||
teardown().
|
||||
|
||||
init_per_testcase(pub_sub_no_acl, Cfg) ->
|
||||
Scheme = http,
|
||||
Inet = inet,
|
||||
http_auth_server:start(Scheme, Inet),
|
||||
Fun = fun(App) -> set_special_configs(App, Scheme, Inet, no_acl) end,
|
||||
emqx_ct_helpers:start_apps([emqx_auth_http], Fun),
|
||||
?assert(is_hooked('client.authenticate')),
|
||||
?assertNot(is_hooked('client.check_acl')),
|
||||
Cfg;
|
||||
init_per_testcase(no_hook_if_config_unset, Cfg) ->
|
||||
setup(http, inet),
|
||||
Cfg;
|
||||
init_per_testcase(_, Cfg) ->
|
||||
%% init per group
|
||||
Cfg.
|
||||
|
||||
end_per_testcase(pub_sub_no_acl, _Cfg) ->
|
||||
teardown();
|
||||
end_per_testcase(no_hook_if_config_unset, _Cfg) ->
|
||||
teardown();
|
||||
end_per_testcase(_, _Cfg) ->
|
||||
%% teardown per group
|
||||
ok.
|
||||
|
||||
setup(Scheme, Inet) ->
|
||||
http_auth_server:start(Scheme, Inet),
|
||||
Fun = fun(App) -> set_special_configs(App, Scheme, Inet, normal) end,
|
||||
emqx_ct_helpers:start_apps([emqx_auth_http], Fun),
|
||||
?assert(is_hooked('client.authenticate')),
|
||||
?assert(is_hooked('client.check_acl')).
|
||||
|
||||
teardown() ->
|
||||
http_auth_server:stop(),
|
||||
application:stop(emqx_auth_http),
|
||||
?assertNot(is_hooked('client.authenticate')),
|
||||
?assertNot(is_hooked('client.check_acl')),
|
||||
emqx_ct_helpers:stop_apps([emqx]).
|
||||
|
||||
set_special_configs(emqx, _Scheme, _Inet, _AuthConfig) ->
|
||||
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(emqx_auth_http, Scheme, Inet, PluginConfig) ->
|
||||
[application:unset_env(?APP, Par) || Par <- [acl_req, auth_req]],
|
||||
ServerAddr = http_server(Scheme, Inet),
|
||||
|
||||
AuthReq = #{method => get,
|
||||
url => ServerAddr ++ "/mqtt/auth",
|
||||
headers => [{"content-type", "application/json"}],
|
||||
params => [{"clientid", "%c"}, {"username", "%u"}, {"password", "%P"}]},
|
||||
SuperReq = #{method => post,
|
||||
url => ServerAddr ++ "/mqtt/superuser",
|
||||
headers => [{"content-type", "application/json"}],
|
||||
params => [{"clientid", "%c"}, {"username", "%u"}]},
|
||||
AclReq = #{method => post,
|
||||
url => ServerAddr ++ "/mqtt/acl",
|
||||
headers => [{"content-type", "application/json"}],
|
||||
params => [{"access", "%A"}, {"username", "%u"}, {"clientid", "%c"}, {"ipaddr", "%a"}, {"topic", "%t"}, {"mountpoint", "%m"}]},
|
||||
|
||||
Scheme =:= https andalso set_https_client_opts(),
|
||||
|
||||
application:set_env(emqx_auth_http, auth_req, maps:to_list(AuthReq)),
|
||||
application:set_env(emqx_auth_http, super_req, maps:to_list(SuperReq)),
|
||||
case PluginConfig of
|
||||
normal -> ok = application:set_env(emqx_auth_http, acl_req, maps:to_list(AclReq));
|
||||
no_acl -> ok
|
||||
end.
|
||||
|
||||
%% @private
|
||||
set_https_client_opts() ->
|
||||
SSLOpt = emqx_ct_helpers:client_ssl_twoway(),
|
||||
application:set_env(emqx_auth_http, cacertfile, proplists:get_value(cacertfile, SSLOpt, undefined)),
|
||||
application:set_env(emqx_auth_http, certfile, proplists:get_value(certfile, SSLOpt, undefined)),
|
||||
application:set_env(emqx_auth_http, keyfile, proplists:get_value(keyfile, SSLOpt, undefined)),
|
||||
application:set_env(emqx_auth_http, verify, true),
|
||||
application:set_env(emqx_auth_http, server_name_indication, "disable").
|
||||
|
||||
%% @private
|
||||
http_server(http, inet) -> "http://127.0.0.1:8991"; % ipv4
|
||||
http_server(http, inet6) -> "http://localhost:8991"; % test hostname resolution
|
||||
http_server(https, inet) -> "https://localhost:8991"; % test hostname resolution
|
||||
http_server(https, inet6) -> "https://[::1]:8991". % ipv6
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
%% Testcases
|
||||
%%------------------------------------------------------------------------------
|
||||
|
||||
t_check_acl(Cfg) when is_list(Cfg) ->
|
||||
SuperUser = ?USER(<<"superclient">>, <<"superuser">>, mqtt, {127,0,0,1}, external),
|
||||
deny = emqx_access_control:check_acl(SuperUser, subscribe, <<"users/testuser/1">>),
|
||||
deny = emqx_access_control:check_acl(SuperUser, publish, <<"anytopic">>),
|
||||
|
||||
User1 = ?USER(<<"client1">>, <<"testuser">>, mqtt, {127,0,0,1}, external),
|
||||
UnIpUser1 = ?USER(<<"client1">>, <<"testuser">>, mqtt, {192,168,0,4}, external),
|
||||
UnClientIdUser1 = ?USER(<<"unkonwc">>, <<"testuser">>, mqtt, {127,0,0,1}, external),
|
||||
UnnameUser1= ?USER(<<"client1">>, <<"unuser">>, mqtt, {127,0,0,1}, external),
|
||||
allow = emqx_access_control:check_acl(User1, subscribe, <<"users/testuser/1">>),
|
||||
deny = emqx_access_control:check_acl(User1, publish, <<"users/testuser/1">>),
|
||||
deny = emqx_access_control:check_acl(UnIpUser1, subscribe, <<"users/testuser/1">>),
|
||||
deny = emqx_access_control:check_acl(UnClientIdUser1, subscribe, <<"users/testuser/1">>),
|
||||
deny = emqx_access_control:check_acl(UnnameUser1, subscribe, <<"$SYS/testuser/1">>),
|
||||
|
||||
User2 = ?USER(<<"client2">>, <<"xyz">>, mqtt, {127,0,0,1}, external),
|
||||
UserC = ?USER(<<"client2">>, <<"xyz">>, mqtt, {192,168,1,3}, external),
|
||||
allow = emqx_access_control:check_acl(UserC, publish, <<"a/b/c">>),
|
||||
deny = emqx_access_control:check_acl(User2, publish, <<"a/b/c">>),
|
||||
deny = emqx_access_control:check_acl(User2, subscribe, <<"$SYS/testuser/1">>).
|
||||
|
||||
t_check_auth(Cfg) when is_list(Cfg) ->
|
||||
User1 = ?USER(<<"client1">>, <<"testuser1">>, mqtt, {127,0,0,1}, external, undefined),
|
||||
User2 = ?USER(<<"client2">>, <<"testuser2">>, mqtt, {127,0,0,1}, exteneral, undefined),
|
||||
User3 = ?USER(<<"client3">>, undefined, mqtt, {127,0,0,1}, exteneral, undefined),
|
||||
|
||||
{ok, #{auth_result := success,
|
||||
anonymous := false,
|
||||
is_superuser := false}} = emqx_access_control:authenticate(User1#{password => <<"pass1">>}),
|
||||
{error, bad_username_or_password} = emqx_access_control:authenticate(User1#{password => <<"pass">>}),
|
||||
{error, bad_username_or_password} = emqx_access_control:authenticate(User1#{password => <<>>}),
|
||||
|
||||
{ok, #{is_superuser := false}} = emqx_access_control:authenticate(User2#{password => <<"pass2">>}),
|
||||
{error, bad_username_or_password} = emqx_access_control:authenticate(User2#{password => <<>>}),
|
||||
{error, bad_username_or_password} = emqx_access_control:authenticate(User2#{password => <<"errorpwd">>}),
|
||||
|
||||
{error, bad_username_or_password} = emqx_access_control:authenticate(User3#{password => <<"pwd">>}).
|
||||
|
||||
pub_sub_no_acl(Cfg) when is_list(Cfg) ->
|
||||
{ok, T1} = emqtt:start_link([{host, "localhost"},
|
||||
{clientid, <<"client1">>},
|
||||
{username, <<"testuser1">>},
|
||||
{password, <<"pass1">>}]),
|
||||
{ok, _} = emqtt:connect(T1),
|
||||
emqtt:publish(T1, <<"topic">>, <<"body">>, [{qos, 0}, {retain, true}]),
|
||||
timer:sleep(1000),
|
||||
{ok, T2} = emqtt:start_link([{host, "localhost"},
|
||||
{clientid, <<"client2">>},
|
||||
{username, <<"testuser2">>},
|
||||
{password, <<"pass2">>}]),
|
||||
{ok, _} = emqtt:connect(T2),
|
||||
emqtt:subscribe(T2, <<"topic">>),
|
||||
receive
|
||||
{publish, _Topic, Payload} ->
|
||||
?assertEqual(<<"body">>, Payload)
|
||||
after 1000 -> false end,
|
||||
emqtt:disconnect(T1),
|
||||
emqtt:disconnect(T2).
|
||||
|
||||
t_pub_sub(Cfg) when is_list(Cfg) ->
|
||||
{ok, T1} = emqtt:start_link([{host, "localhost"},
|
||||
{clientid, <<"client1">>},
|
||||
{username, <<"testuser1">>},
|
||||
{password, <<"pass1">>}]),
|
||||
{ok, _} = emqtt:connect(T1),
|
||||
emqtt:publish(T1, <<"topic">>, <<"body">>, [{qos, 0}, {retain, true}]),
|
||||
timer:sleep(1000),
|
||||
{ok, T2} = emqtt:start_link([{host, "localhost"},
|
||||
{clientid, <<"client2">>},
|
||||
{username, <<"testuser2">>},
|
||||
{password, <<"pass2">>}]),
|
||||
{ok, _} = emqtt:connect(T2),
|
||||
emqtt:subscribe(T2, <<"topic">>),
|
||||
receive
|
||||
{publish, _Topic, Payload} ->
|
||||
?assertEqual(<<"body">>, Payload)
|
||||
after 1000 -> false end,
|
||||
emqtt:disconnect(T1),
|
||||
emqtt:disconnect(T2).
|
||||
|
||||
no_hook_if_config_unset(Cfg) when is_list(Cfg) ->
|
||||
?assert(is_hooked('client.authenticate')),
|
||||
?assert(is_hooked('client.check_acl')),
|
||||
application:stop(?APP),
|
||||
[application:unset_env(?APP, Par) || Par <- [acl_req, auth_req]],
|
||||
application:start(?APP),
|
||||
?assertEqual([], emqx_hooks:lookup('client.authenticate')),
|
||||
?assertNot(is_hooked('client.authenticate')),
|
||||
?assertNot(is_hooked('client.check_acl')).
|
||||
|
||||
is_hooked(HookName) ->
|
||||
Callbacks = emqx_hooks:lookup(HookName),
|
||||
F = fun(Callback) ->
|
||||
case emqx_hooks:callback_action(Callback) of
|
||||
{emqx_auth_http, check, _} ->
|
||||
'client.authenticate' = HookName, % assert
|
||||
true;
|
||||
{emqx_acl_http, check_acl, _} ->
|
||||
'client.check_acl' = HookName, % assert
|
||||
true;
|
||||
_ ->
|
||||
false
|
||||
end
|
||||
end,
|
||||
case lists:filter(F, Callbacks) of
|
||||
[_] -> true;
|
||||
[] -> false
|
||||
end.
|
|
@ -1,152 +0,0 @@
|
|||
-module(http_auth_server).
|
||||
|
||||
-export([ start/2
|
||||
, stop/0
|
||||
]).
|
||||
|
||||
-define(SUPERUSER, [[{"username", "superuser"}, {"clientid", "superclient"}]]).
|
||||
|
||||
-define(ACL, [[{<<"username">>, <<"testuser">>},
|
||||
{<<"clientid">>, <<"client1">>},
|
||||
{<<"access">>, <<"1">>},
|
||||
{<<"topic">>, <<"users/testuser/1">>},
|
||||
{<<"ipaddr">>, <<"127.0.0.1">>},
|
||||
{<<"mountpoint">>, <<"null">>}],
|
||||
[{<<"username">>, <<"xyz">>},
|
||||
{<<"clientid">>, <<"client2">>},
|
||||
{<<"access">>, <<"2">>},
|
||||
{<<"topic">>, <<"a/b/c">>},
|
||||
{<<"ipaddr">>, <<"192.168.1.3">>},
|
||||
{<<"mountpoint">>, <<"null">>}],
|
||||
[{<<"username">>, <<"testuser1">>},
|
||||
{<<"clientid">>, <<"client1">>},
|
||||
{<<"access">>, <<"2">>},
|
||||
{<<"topic">>, <<"topic">>},
|
||||
{<<"ipaddr">>, <<"127.0.0.1">>},
|
||||
{<<"mountpoint">>, <<"null">>}],
|
||||
[{<<"username">>, <<"testuser2">>},
|
||||
{<<"clientid">>, <<"client2">>},
|
||||
{<<"access">>, <<"1">>},
|
||||
{<<"topic">>, <<"topic">>},
|
||||
{<<"ipaddr">>, <<"127.0.0.1">>},
|
||||
{<<"mountpoint">>, <<"null">>}]]).
|
||||
|
||||
-define(AUTH, [[{<<"clientid">>, <<"client1">>},
|
||||
{<<"username">>, <<"testuser1">>},
|
||||
{<<"password">>, <<"pass1">>}],
|
||||
[{<<"clientid">>, <<"client2">>},
|
||||
{<<"username">>, <<"testuser2">>},
|
||||
{<<"password">>, <<"pass2">>}]]).
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
%% REST Interface
|
||||
%%------------------------------------------------------------------------------
|
||||
|
||||
-rest_api(#{ name => auth
|
||||
, method => 'GET'
|
||||
, path => "/mqtt/auth"
|
||||
, func => authenticate
|
||||
, descr => "Authenticate user access permission"
|
||||
}).
|
||||
|
||||
-rest_api(#{ name => is_superuser
|
||||
, method => 'GET'
|
||||
, path => "/mqtt/superuser"
|
||||
, func => is_superuser
|
||||
, descr => "Is super user"
|
||||
}).
|
||||
|
||||
-rest_api(#{ name => acl
|
||||
, method => 'GET'
|
||||
, path => "/mqtt/acl"
|
||||
, func => check_acl
|
||||
, descr => "Check acl"
|
||||
}).
|
||||
|
||||
-rest_api(#{ name => auth
|
||||
, method => 'POST'
|
||||
, path => "/mqtt/auth"
|
||||
, func => authenticate
|
||||
, descr => "Authenticate user access permission"
|
||||
}).
|
||||
|
||||
-rest_api(#{ name => is_superuser
|
||||
, method => 'POST'
|
||||
, path => "/mqtt/superuser"
|
||||
, func => is_superuser
|
||||
, descr => "Is super user"
|
||||
}).
|
||||
|
||||
-rest_api(#{ name => acl
|
||||
, method => 'POST'
|
||||
, path => "/mqtt/acl"
|
||||
, func => check_acl
|
||||
, descr => "Check acl"
|
||||
}).
|
||||
|
||||
-export([ authenticate/2
|
||||
, is_superuser/2
|
||||
, check_acl/2
|
||||
]).
|
||||
|
||||
authenticate(_Binding, Params) ->
|
||||
return(check(Params, ?AUTH)).
|
||||
|
||||
is_superuser(_Binding, Params) ->
|
||||
return(check(Params, ?SUPERUSER)).
|
||||
|
||||
check_acl(_Binding, Params) ->
|
||||
return(check(Params, ?ACL)).
|
||||
|
||||
return(allow) -> {200, <<"allow">>};
|
||||
return(deny) -> {400, <<"deny">>}.
|
||||
|
||||
start(http, Inet) ->
|
||||
application:ensure_all_started(minirest),
|
||||
Handlers = [{"/", minirest:handler(#{modules => [?MODULE]})}],
|
||||
Dispatch = [{"/[...]", minirest, Handlers}],
|
||||
minirest:start_http(http_auth_server, #{socket_opts => [Inet, {port, 8991}]}, Dispatch);
|
||||
|
||||
start(https, Inet) ->
|
||||
application:ensure_all_started(minirest),
|
||||
Handlers = [{"/", minirest:handler(#{modules => [?MODULE]})}],
|
||||
Dispatch = [{"/[...]", minirest, Handlers}],
|
||||
minirest:start_https(http_auth_server, #{socket_opts => [Inet, {port, 8991} | certopts()]}, Dispatch).
|
||||
|
||||
%% @private
|
||||
certopts() ->
|
||||
Certfile = filename:join(["etc", "certs", "cert.pem"]),
|
||||
Keyfile = filename:join(["etc", "certs", "key.pem"]),
|
||||
CaCert = filename:join(["etc", "certs", "cacert.pem"]),
|
||||
[{verify, verify_peer},
|
||||
{certfile, emqx_ct_helpers:deps_path(emqx, Certfile)},
|
||||
{keyfile, emqx_ct_helpers:deps_path(emqx, Keyfile)},
|
||||
{cacertfile, emqx_ct_helpers:deps_path(emqx, CaCert)}] ++ emqx_ct_helpers:client_ssl().
|
||||
|
||||
stop() ->
|
||||
minirest:stop_http(http_auth_server).
|
||||
|
||||
-spec check(HttpReqParams :: list(), DefinedConf :: list()) -> allow | deny.
|
||||
check(_Params, []) ->
|
||||
%ct:pal("check auth_result: deny~n"),
|
||||
deny;
|
||||
check(Params, [ConfRecord|T]) ->
|
||||
% ct:pal("Params: ~p, ConfRecord:~p ~n", [Params, ConfRecord]),
|
||||
case match_config(Params, ConfRecord) of
|
||||
not_match ->
|
||||
check(Params, T);
|
||||
matched -> allow
|
||||
end.
|
||||
|
||||
match_config([], _ConfigColumn) ->
|
||||
%ct:pal("match_config auth_result: matched~n"),
|
||||
matched;
|
||||
|
||||
match_config([Param|T], ConfigColumn) ->
|
||||
%ct:pal("Param: ~p, ConfigColumn:~p ~n", [Param, ConfigColumn]),
|
||||
case lists:member(Param, ConfigColumn) of
|
||||
true ->
|
||||
match_config(T, ConfigColumn);
|
||||
false ->
|
||||
not_match
|
||||
end.
|
|
@ -1,28 +0,0 @@
|
|||
.eunit
|
||||
deps
|
||||
*.o
|
||||
*.beam
|
||||
*.plt
|
||||
erl_crash.dump
|
||||
ebin
|
||||
rel/example_project
|
||||
.concrete/DEV_MODE
|
||||
.rebar
|
||||
.erlang.mk/
|
||||
emqx_auth_jwt.d
|
||||
data/
|
||||
.DS_Store
|
||||
cover/
|
||||
ct.coverdata
|
||||
eunit.coverdata
|
||||
logs/
|
||||
test/ct.cover.spec
|
||||
emq_auth_jwt.d
|
||||
erlang.mk
|
||||
_build/
|
||||
rebar.lock
|
||||
rebar3.crashdump
|
||||
etc/emqx_auth_jwt.conf.rendered
|
||||
.rebar3/
|
||||
*.swp
|
||||
Mnesia.nonode@nohost/
|
|
@ -1,90 +0,0 @@
|
|||
|
||||
# emqx-auth-jwt
|
||||
|
||||
EMQ X JWT Authentication Plugin
|
||||
|
||||
Build
|
||||
-----
|
||||
|
||||
```
|
||||
make && make tests
|
||||
```
|
||||
|
||||
Configure the Plugin
|
||||
--------------------
|
||||
|
||||
File: etc/plugins/emqx_auth_jwt.conf
|
||||
|
||||
```
|
||||
## HMAC Hash Secret.
|
||||
##
|
||||
## Value: String
|
||||
auth.jwt.secret = emqxsecret
|
||||
|
||||
## From where the JWT string can be got
|
||||
##
|
||||
## Value: username | password
|
||||
## Default: password
|
||||
auth.jwt.from = password
|
||||
|
||||
## RSA or ECDSA public key file.
|
||||
##
|
||||
## Value: File
|
||||
## auth.jwt.pubkey = etc/certs/jwt_public_key.pem
|
||||
|
||||
## Enable to verify claims fields
|
||||
##
|
||||
## Value: on | off
|
||||
auth.jwt.verify_claims = off
|
||||
|
||||
## The checklist of claims to validate
|
||||
##
|
||||
## Value: String
|
||||
## auth.jwt.verify_claims.$name = expected
|
||||
##
|
||||
## Variables:
|
||||
## - %u: username
|
||||
## - %c: clientid
|
||||
# auth.jwt.verify_claims.username = %u
|
||||
```
|
||||
|
||||
Load the Plugin
|
||||
---------------
|
||||
|
||||
```
|
||||
./bin/emqx_ctl plugins load emqx_auth_jwt
|
||||
```
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
```
|
||||
mosquitto_pub -t 'pub' -m 'hello' -i test -u test -P eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiYm9iIiwiYWdlIjoyOX0.bIV_ZQ8D5nQi0LT8AVkpM4Pd6wmlbpR9S8nOLJAsA8o
|
||||
```
|
||||
|
||||
Algorithms
|
||||
----------
|
||||
|
||||
The JWT spec supports several algorithms for cryptographic signing. This plugin currently supports:
|
||||
|
||||
* HS256 - HMAC using SHA-256 hash algorithm
|
||||
* HS384 - HMAC using SHA-384 hash algorithm
|
||||
* HS512 - HMAC using SHA-512 hash algorithm
|
||||
|
||||
* RS256 - RSA with the SHA-256 hash algorithm
|
||||
* RS384 - RSA with the SHA-384 hash algorithm
|
||||
* RS512 - RSA with the SHA-512 hash algorithm
|
||||
|
||||
* ES256 - ECDSA using the P-256 curve
|
||||
* ES384 - ECDSA using the P-384 curve
|
||||
* ES512 - ECDSA using the P-512 curve
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
Apache License Version 2.0
|
||||
|
||||
Author
|
||||
------
|
||||
|
||||
EMQ X Team.
|
|
@ -1,2 +0,0 @@
|
|||
1. Notice for the [Critical vulnerabilities in JSON Web Token](https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/)
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
|
||||
https://crypto.stackexchange.com/questions/30657/hmac-vs-ecdsa-for-jwt
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
##--------------------------------------------------------------------
|
||||
## JWT Auth Plugin
|
||||
##--------------------------------------------------------------------
|
||||
|
||||
## HMAC Hash Secret.
|
||||
##
|
||||
## Value: String
|
||||
auth.jwt.secret = emqxsecret
|
||||
|
||||
## RSA or ECDSA public key file.
|
||||
##
|
||||
## Value: File
|
||||
#auth.jwt.pubkey = "etc/certs/jwt_public_key.pem"
|
||||
|
||||
## The JWKs server address
|
||||
##
|
||||
## see: http://self-issued.info/docs/draft-ietf-jose-json-web-key.html
|
||||
##
|
||||
#auth.jwt.jwks.endpoint = "https://127.0.0.1:8080/jwks"
|
||||
|
||||
## The JWKs refresh interval
|
||||
##
|
||||
## Value: Duration
|
||||
#auth.jwt.jwks.refresh_interval = 5m
|
||||
|
||||
## From where the JWT string can be got
|
||||
##
|
||||
## Value: username | password
|
||||
## Default: password
|
||||
auth.jwt.from = password
|
||||
|
||||
## Enable to verify claims fields
|
||||
##
|
||||
## Value: on | off
|
||||
auth.jwt.verify_claims.enable = off
|
||||
|
||||
## The checklist of claims to validate
|
||||
##
|
||||
## Configuration format: auth.jwt.verify_claims.$name = $expected
|
||||
## - $name: the name of the field in the JWT payload to be verified
|
||||
## - $expected: the expected value
|
||||
##
|
||||
## The available placeholders for $expected:
|
||||
## - %u: username
|
||||
## - %c: clientid
|
||||
##
|
||||
## For example, to verify that the username in the JWT payload is the same
|
||||
## as the client (MQTT protocol) username
|
||||
#auth.jwt.verify_claims.username = "%u"
|
|
@ -1,49 +0,0 @@
|
|||
%%-*- mode: erlang -*-
|
||||
|
||||
{mapping, "auth.jwt.secret", "emqx_auth_jwt.secret", [
|
||||
{datatype, string}
|
||||
]}.
|
||||
|
||||
{mapping, "auth.jwt.jwks.endpoint", "emqx_auth_jwt.jwks", [
|
||||
{datatype, string}
|
||||
]}.
|
||||
|
||||
{mapping, "auth.jwt.jwks.refresh_interval", "emqx_auth_jwt.refresh_interval", [
|
||||
{datatype, {duration, ms}}
|
||||
]}.
|
||||
|
||||
{mapping, "auth.jwt.from", "emqx_auth_jwt.from", [
|
||||
{default, password},
|
||||
{datatype, atom}
|
||||
]}.
|
||||
|
||||
{mapping, "auth.jwt.pubkey", "emqx_auth_jwt.pubkey", [
|
||||
{datatype, string}
|
||||
]}.
|
||||
|
||||
{mapping, "auth.jwt.signature_format", "emqx_auth_jwt.jwerl_opts", [
|
||||
{default, "der"},
|
||||
{datatype, {enum, [raw, der]}}
|
||||
]}.
|
||||
|
||||
{mapping, "auth.jwt.verify_claims.enable", "emqx_auth_jwt.verify_claims", [
|
||||
{default, off},
|
||||
{datatype, flag}
|
||||
]}.
|
||||
|
||||
{mapping, "auth.jwt.verify_claims.$name", "emqx_auth_jwt.verify_claims", [
|
||||
{datatype, string}
|
||||
]}.
|
||||
|
||||
{translation, "emqx_auth_jwt.verify_claims", fun(Conf) ->
|
||||
case cuttlefish:conf_get("auth.jwt.verify_claims.enable", Conf) of
|
||||
false -> cuttlefish:unset();
|
||||
true ->
|
||||
lists:foldr(
|
||||
fun({["auth","jwt","verify_claims", Name], Value}, Acc) ->
|
||||
[{list_to_atom(Name), list_to_binary(Value)} | Acc];
|
||||
({["auth","jwt","verify_claims"], _Value}, Acc) ->
|
||||
Acc
|
||||
end, [], cuttlefish_variable:filter_by_prefix("auth.jwt.verify_claims", Conf))
|
||||
end
|
||||
end}.
|
|
@ -1,25 +0,0 @@
|
|||
{deps,
|
||||
[
|
||||
{jose, {git, "https://github.com/potatosalad/erlang-jose", {tag, "1.11.1"}}}
|
||||
]}.
|
||||
|
||||
{edoc_opts, [{preprocess, true}]}.
|
||||
{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}.
|
||||
|
||||
{profiles,
|
||||
[{test,
|
||||
[{deps, []}
|
||||
]}
|
||||
]}.
|
|
@ -1,14 +0,0 @@
|
|||
{application, emqx_auth_jwt,
|
||||
[{description, "EMQ X Authentication with JWT"},
|
||||
{vsn, "4.4.0"}, % strict semver, bump manually!
|
||||
{modules, []},
|
||||
{registered, [emqx_auth_jwt_sup]},
|
||||
{applications, [kernel,stdlib,jose]},
|
||||
{mod, {emqx_auth_jwt_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-jwt"}
|
||||
]}
|
||||
]}.
|
|
@ -1,15 +0,0 @@
|
|||
%% -*-: erlang -*-
|
||||
{VSN,
|
||||
[
|
||||
{"4.3.0", [
|
||||
{load_module, emqx_auth_jwt_svr, brutal_purge, soft_purge, []}
|
||||
]},
|
||||
{<<".*">>, []}
|
||||
],
|
||||
[
|
||||
{"4.3.0", [
|
||||
{load_module, emqx_auth_jwt_svr, brutal_purge, soft_purge, []}
|
||||
]},
|
||||
{<<".*">>, []}
|
||||
]
|
||||
}.
|
|
@ -1,99 +0,0 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_auth_jwt).
|
||||
|
||||
-include_lib("emqx/include/emqx.hrl").
|
||||
-include_lib("emqx/include/logger.hrl").
|
||||
|
||||
-logger_header("[JWT]").
|
||||
|
||||
-export([ register_metrics/0
|
||||
, check/3
|
||||
, description/0
|
||||
]).
|
||||
|
||||
-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)).
|
||||
|
||||
-spec(register_metrics() -> ok).
|
||||
register_metrics() ->
|
||||
lists:foreach(fun emqx_metrics:ensure/1, ?AUTH_METRICS).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Authentication callbacks
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
check(ClientInfo, AuthResult, #{pid := Pid,
|
||||
from := From,
|
||||
checklists := Checklists}) ->
|
||||
case maps:find(From, ClientInfo) of
|
||||
error ->
|
||||
ok = emqx_metrics:inc(?AUTH_METRICS(ignore));
|
||||
{ok, undefined} ->
|
||||
ok = emqx_metrics:inc(?AUTH_METRICS(ignore));
|
||||
{ok, Token} ->
|
||||
case emqx_auth_jwt_svr:verify(Pid, Token) of
|
||||
{error, not_found} ->
|
||||
ok = emqx_metrics:inc(?AUTH_METRICS(ignore));
|
||||
{error, not_token} ->
|
||||
ok = emqx_metrics:inc(?AUTH_METRICS(ignore));
|
||||
{error, Reason} ->
|
||||
ok = emqx_metrics:inc(?AUTH_METRICS(failure)),
|
||||
{stop, AuthResult#{auth_result => Reason, anonymous => false}};
|
||||
{ok, Claims} ->
|
||||
{stop, maps:merge(AuthResult, verify_claims(Checklists, Claims, ClientInfo))}
|
||||
end
|
||||
end.
|
||||
|
||||
description() -> "Authentication with JWT".
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
%% Verify Claims
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
verify_claims(Checklists, Claims, ClientInfo) ->
|
||||
case do_verify_claims(feedvar(Checklists, ClientInfo), Claims) of
|
||||
{error, Reason} ->
|
||||
ok = emqx_metrics:inc(?AUTH_METRICS(failure)),
|
||||
#{auth_result => Reason, anonymous => false};
|
||||
ok ->
|
||||
ok = emqx_metrics:inc(?AUTH_METRICS(success)),
|
||||
#{auth_result => success, anonymous => false, jwt_claims => Claims}
|
||||
end.
|
||||
|
||||
do_verify_claims([], _Claims) ->
|
||||
ok;
|
||||
do_verify_claims([{Key, Expected} | L], Claims) ->
|
||||
case maps:get(Key, Claims, undefined) =:= Expected of
|
||||
true -> do_verify_claims(L, Claims);
|
||||
false -> {error, {verify_claim_failed, Key}}
|
||||
end.
|
||||
|
||||
feedvar(Checklists, #{username := Username, clientid := ClientId}) ->
|
||||
lists:map(fun({K, <<"%u">>}) -> {K, Username};
|
||||
({K, <<"%c">>}) -> {K, ClientId};
|
||||
({K, Expected}) -> {K, Expected}
|
||||
end, Checklists).
|
|
@ -1,81 +0,0 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_auth_jwt_app).
|
||||
|
||||
-behaviour(application).
|
||||
|
||||
-behaviour(supervisor).
|
||||
|
||||
-emqx_plugin(auth).
|
||||
|
||||
-export([start/2, stop/1]).
|
||||
|
||||
-export([init/1]).
|
||||
|
||||
-define(APP, emqx_auth_jwt).
|
||||
|
||||
start(_Type, _Args) ->
|
||||
{ok, Sup} = supervisor:start_link({local, ?MODULE}, ?MODULE, []),
|
||||
|
||||
{ok, Pid} = start_auth_server(jwks_svr_options()),
|
||||
ok = emqx_auth_jwt:register_metrics(),
|
||||
AuthEnv0 = auth_env(),
|
||||
AuthEnv1 = AuthEnv0#{pid => Pid},
|
||||
|
||||
_ = emqx:hook('client.authenticate', {emqx_auth_jwt, check, [AuthEnv1]}),
|
||||
{ok, Sup, AuthEnv1}.
|
||||
|
||||
stop(AuthEnv) ->
|
||||
emqx:unhook('client.authenticate', {emqx_auth_jwt, check, [AuthEnv]}).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Dummy supervisor
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
init([]) ->
|
||||
{ok, {{one_for_all, 1, 10}, []}}.
|
||||
|
||||
start_auth_server(Options) ->
|
||||
Spec = #{id => jwt_svr,
|
||||
start => {emqx_auth_jwt_svr, start_link, [Options]},
|
||||
restart => permanent,
|
||||
shutdown => brutal_kill,
|
||||
type => worker,
|
||||
modules => [emqx_auth_jwt_svr]},
|
||||
supervisor:start_child(?MODULE, Spec).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Internal functions
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
auth_env() ->
|
||||
Checklists = [{atom_to_binary(K, utf8), V}
|
||||
|| {K, V} <- env(verify_claims, [])],
|
||||
#{ from => env(from, password)
|
||||
, checklists => Checklists
|
||||
}.
|
||||
|
||||
jwks_svr_options() ->
|
||||
[{K, V} || {K, V}
|
||||
<- [{secret, env(secret, undefined)},
|
||||
{pubkey, env(pubkey, undefined)},
|
||||
{jwks_addr, env(jwks, undefined)},
|
||||
{interval, env(refresh_interval, undefined)}],
|
||||
V /= undefined].
|
||||
|
||||
env(Key, Default) ->
|
||||
application:get_env(?APP, Key, Default).
|
|
@ -1,224 +0,0 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_auth_jwt_svr).
|
||||
|
||||
-behaviour(gen_server).
|
||||
|
||||
-include_lib("emqx/include/logger.hrl").
|
||||
-include_lib("jose/include/jose_jwk.hrl").
|
||||
|
||||
-logger_header("[JWT-SVR]").
|
||||
|
||||
%% APIs
|
||||
-export([start_link/1]).
|
||||
|
||||
-export([verify/2]).
|
||||
|
||||
%% gen_server callbacks
|
||||
-export([ init/1
|
||||
, handle_call/3
|
||||
, handle_cast/2
|
||||
, handle_info/2
|
||||
, terminate/2
|
||||
, code_change/3
|
||||
]).
|
||||
|
||||
-type options() :: [option()].
|
||||
-type option() :: {secret, list()}
|
||||
| {pubkey, list()}
|
||||
| {jwks_addr, list()}
|
||||
| {interval, pos_integer()}.
|
||||
|
||||
-define(INTERVAL, 300000).
|
||||
|
||||
-record(state, {static, remote, addr, tref, intv}).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% APIs
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-spec start_link(options()) -> gen_server:start_ret().
|
||||
start_link(Options) ->
|
||||
gen_server:start_link(?MODULE, [Options], []).
|
||||
|
||||
-spec verify(pid(), binary())
|
||||
-> {error, term()}
|
||||
| {ok, Payload :: map()}.
|
||||
verify(S, JwsCompacted) when is_binary(JwsCompacted) ->
|
||||
case catch jose_jws:peek(JwsCompacted) of
|
||||
{'EXIT', _} -> {error, not_token};
|
||||
_ -> gen_server:call(S, {verify, JwsCompacted})
|
||||
end.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% gen_server callbacks
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
init([Options]) ->
|
||||
ok = jose:json_module(jiffy),
|
||||
{Static, Remote} = do_init_jwks(Options),
|
||||
Intv = proplists:get_value(interval, Options, ?INTERVAL),
|
||||
{ok, reset_timer(
|
||||
#state{
|
||||
static = Static,
|
||||
remote = Remote,
|
||||
addr = proplists:get_value(jwks_addr, Options),
|
||||
intv = Intv})}.
|
||||
|
||||
%% @private
|
||||
do_init_jwks(Options) ->
|
||||
K2J = fun(K, F) ->
|
||||
case proplists:get_value(K, Options) of
|
||||
undefined -> undefined;
|
||||
V ->
|
||||
try F(V) of
|
||||
{error, Reason} ->
|
||||
?LOG(warning, "Build ~p JWK ~p failed: {error, ~p}~n",
|
||||
[K, V, Reason]),
|
||||
undefined;
|
||||
J -> J
|
||||
catch T:R:_ ->
|
||||
?LOG(warning, "Build ~p JWK ~p failed: {~p, ~p}~n",
|
||||
[K, V, T, R]),
|
||||
undefined
|
||||
end
|
||||
end
|
||||
end,
|
||||
OctJwk = K2J(secret, fun(V) ->
|
||||
jose_jwk:from_oct(list_to_binary(V))
|
||||
end),
|
||||
PemJwk = K2J(pubkey, fun jose_jwk:from_pem_file/1),
|
||||
Remote = K2J(jwks_addr, fun request_jwks/1),
|
||||
{[J ||J <- [OctJwk, PemJwk], J /= undefined], Remote}.
|
||||
|
||||
handle_call({verify, JwsCompacted}, _From, State) ->
|
||||
handle_verify(JwsCompacted, State);
|
||||
|
||||
handle_call(_Req, _From, State) ->
|
||||
{reply, ok, State}.
|
||||
|
||||
handle_cast(_Msg, State) ->
|
||||
{noreply, State}.
|
||||
|
||||
handle_info({timeout, _TRef, refresh}, State = #state{addr = Addr}) ->
|
||||
NState = try
|
||||
State#state{remote = request_jwks(Addr)}
|
||||
catch _:_ ->
|
||||
State
|
||||
end,
|
||||
{noreply, reset_timer(NState)};
|
||||
|
||||
handle_info(_Info, State) ->
|
||||
{noreply, State}.
|
||||
|
||||
terminate(_Reason, State) ->
|
||||
_ = cancel_timer(State),
|
||||
ok.
|
||||
|
||||
code_change(_OldVsn, State, _Extra) ->
|
||||
{ok, State}.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Internal funcs
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
handle_verify(JwsCompacted,
|
||||
State = #state{static = Static, remote = Remote}) ->
|
||||
try
|
||||
Jwks = case emqx_json:decode(jose_jws:peek_protected(JwsCompacted), [return_maps]) of
|
||||
#{<<"kid">> := Kid} when Remote /= undefined ->
|
||||
[J || J <- Remote, maps:get(<<"kid">>, J#jose_jwk.fields, undefined) =:= Kid];
|
||||
_ -> Static
|
||||
end,
|
||||
case Jwks of
|
||||
[] -> {reply, {error, not_found}, State};
|
||||
_ ->
|
||||
{reply, do_verify(JwsCompacted, Jwks), State}
|
||||
end
|
||||
catch
|
||||
Class : Reason : Stk ->
|
||||
?LOG(error, "Handle JWK crashed: ~p, ~p, stacktrace: ~p~n",
|
||||
[Class, Reason, Stk]),
|
||||
{reply, {error, invalid_signature}, State}
|
||||
end.
|
||||
|
||||
request_jwks(Addr) ->
|
||||
case httpc:request(get, {Addr, []}, [], [{body_format, binary}]) of
|
||||
{error, Reason} ->
|
||||
error(Reason);
|
||||
{ok, {_Code, _Headers, Body}} ->
|
||||
try
|
||||
JwkSet = jose_jwk:from(emqx_json:decode(Body, [return_maps])),
|
||||
{_, Jwks} = JwkSet#jose_jwk.keys, Jwks
|
||||
catch _:_ ->
|
||||
?LOG(error, "Invalid jwks server response: ~p~n", [Body]),
|
||||
error(badarg)
|
||||
end
|
||||
end.
|
||||
|
||||
reset_timer(State = #state{addr = undefined}) ->
|
||||
State;
|
||||
reset_timer(State = #state{intv = Intv}) ->
|
||||
State#state{tref = erlang:start_timer(Intv, self(), refresh)}.
|
||||
|
||||
cancel_timer(State = #state{tref = undefined}) ->
|
||||
State;
|
||||
cancel_timer(State = #state{tref = TRef}) ->
|
||||
_ = erlang:cancel_timer(TRef),
|
||||
State#state{tref = undefined}.
|
||||
|
||||
do_verify(_JwsCompated, []) ->
|
||||
{error, invalid_signature};
|
||||
do_verify(JwsCompacted, [Jwk|More]) ->
|
||||
case jose_jws:verify(Jwk, JwsCompacted) of
|
||||
{true, Payload, _Jws} ->
|
||||
Claims = emqx_json:decode(Payload, [return_maps]),
|
||||
case check_claims(Claims) of
|
||||
{false, <<"exp">>} ->
|
||||
{error, {invalid_signature, expired}};
|
||||
NClaims ->
|
||||
{ok, NClaims}
|
||||
end;
|
||||
{false, _, _} ->
|
||||
do_verify(JwsCompacted, More)
|
||||
end.
|
||||
|
||||
check_claims(Claims) ->
|
||||
Now = os:system_time(seconds),
|
||||
Checker = [{<<"exp">>, fun(ExpireTime) ->
|
||||
Now < ExpireTime
|
||||
end},
|
||||
{<<"iat">>, fun(IssueAt) ->
|
||||
IssueAt =< Now
|
||||
end},
|
||||
{<<"nbf">>, fun(NotBefore) ->
|
||||
NotBefore =< Now
|
||||
end}
|
||||
],
|
||||
do_check_claim(Checker, Claims).
|
||||
|
||||
do_check_claim([], Claims) ->
|
||||
Claims;
|
||||
do_check_claim([{K, F}|More], Claims) ->
|
||||
case maps:take(K, Claims) of
|
||||
error -> do_check_claim(More, Claims);
|
||||
{V, NClaims} ->
|
||||
case F(V) of
|
||||
true -> do_check_claim(More, NClaims);
|
||||
_ -> {false, K}
|
||||
end
|
||||
end.
|
|
@ -1,166 +0,0 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_auth_jwt_SUITE).
|
||||
|
||||
-compile(export_all).
|
||||
-compile(nowarn_export_all).
|
||||
|
||||
-include_lib("emqx/include/emqx.hrl").
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
-include_lib("common_test/include/ct.hrl").
|
||||
|
||||
-define(APP, emqx_auth_jwt).
|
||||
|
||||
all() ->
|
||||
[{group, emqx_auth_jwt}].
|
||||
|
||||
groups() ->
|
||||
[{emqx_auth_jwt, [sequence], [ t_check_auth
|
||||
, t_check_claims
|
||||
, t_check_claims_clientid
|
||||
, t_check_claims_username
|
||||
, t_check_claims_kid_in_header
|
||||
]}
|
||||
].
|
||||
|
||||
init_per_suite(Config) ->
|
||||
emqx_ct_helpers:start_apps([emqx_auth_jwt], fun set_special_configs/1),
|
||||
Config.
|
||||
|
||||
end_per_suite(_Config) ->
|
||||
emqx_ct_helpers:stop_apps([emqx_auth_jwt]).
|
||||
|
||||
set_special_configs(emqx) ->
|
||||
application:set_env(emqx, allow_anonymous, false),
|
||||
application:set_env(emqx, acl_nomatch, deny),
|
||||
application:set_env(emqx, enable_acl_cache, false),
|
||||
LoadedPluginPath = filename:join(["test", "emqx_SUITE_data", "loaded_plugins"]),
|
||||
AclFilePath = filename:join(["test", "emqx_SUITE_data", "acl.conf"]),
|
||||
application:set_env(emqx, plugins_loaded_file,
|
||||
emqx_ct_helpers:deps_path(emqx, LoadedPluginPath)),
|
||||
application:set_env(emqx, acl_file,
|
||||
emqx_ct_helpers:deps_path(emqx, AclFilePath));
|
||||
|
||||
set_special_configs(emqx_auth_jwt) ->
|
||||
application:set_env(emqx_auth_jwt, secret, "emqxsecret"),
|
||||
application:set_env(emqx_auth_jwt, from, password);
|
||||
|
||||
set_special_configs(_) ->
|
||||
ok.
|
||||
|
||||
sign(Payload, Header, Key) when is_map(Header) ->
|
||||
Jwk = jose_jwk:from_oct(Key),
|
||||
Jwt = emqx_json:encode(Payload),
|
||||
{_, Token} = jose_jws:compact(jose_jwt:sign(Jwk, Header, Jwt)),
|
||||
Token;
|
||||
|
||||
sign(Payload, Alg, Key) ->
|
||||
Jwk = jose_jwk:from_oct(Key),
|
||||
Jwt = emqx_json:encode(Payload),
|
||||
{_, Token} = jose_jws:compact(jose_jwt:sign(Jwk, #{<<"alg">> => Alg}, Jwt)),
|
||||
Token.
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
%% Testcases
|
||||
%%------------------------------------------------------------------------------
|
||||
|
||||
t_check_auth(_) ->
|
||||
Plain = #{clientid => <<"client1">>, username => <<"plain">>, zone => external},
|
||||
Jwt = sign([{clientid, <<"client1">>},
|
||||
{username, <<"plain">>},
|
||||
{exp, os:system_time(seconds) + 3}], <<"HS256">>, <<"emqxsecret">>),
|
||||
ct:pal("Jwt: ~p~n", [Jwt]),
|
||||
|
||||
Result0 = emqx_access_control:authenticate(Plain#{password => Jwt}),
|
||||
ct:pal("Auth result: ~p~n", [Result0]),
|
||||
?assertMatch({ok, #{auth_result := success, jwt_claims := #{<<"clientid">> := <<"client1">>}}}, Result0),
|
||||
|
||||
ct:sleep(3100),
|
||||
Result1 = emqx_access_control:authenticate(Plain#{password => Jwt}),
|
||||
ct:pal("Auth result after 1000ms: ~p~n", [Result1]),
|
||||
?assertMatch({error, _}, Result1),
|
||||
|
||||
Jwt_Error = sign([{client_id, <<"client1">>},
|
||||
{username, <<"plain">>}], <<"HS256">>, <<"secret">>),
|
||||
ct:pal("invalid jwt: ~p~n", [Jwt_Error]),
|
||||
Result2 = emqx_access_control:authenticate(Plain#{password => Jwt_Error}),
|
||||
ct:pal("Auth result for the invalid jwt: ~p~n", [Result2]),
|
||||
?assertEqual({error, invalid_signature}, Result2),
|
||||
?assertMatch({error, _}, emqx_access_control:authenticate(Plain#{password => <<"asd">>})).
|
||||
|
||||
t_check_claims(_) ->
|
||||
application:set_env(emqx_auth_jwt, verify_claims, [{sub, <<"value">>}]),
|
||||
application:stop(emqx_auth_jwt), application:start(emqx_auth_jwt),
|
||||
|
||||
Plain = #{clientid => <<"client1">>, username => <<"plain">>, zone => external},
|
||||
Jwt = sign([{client_id, <<"client1">>},
|
||||
{username, <<"plain">>},
|
||||
{sub, value},
|
||||
{exp, os:system_time(seconds) + 3}], <<"HS256">>, <<"emqxsecret">>),
|
||||
Result0 = emqx_access_control:authenticate(Plain#{password => Jwt}),
|
||||
ct:pal("Auth result: ~p~n", [Result0]),
|
||||
?assertMatch({ok, #{auth_result := success, jwt_claims := _}}, Result0),
|
||||
Jwt_Error = sign([{clientid, <<"client1">>},
|
||||
{username, <<"plain">>}], <<"HS256">>, <<"secret">>),
|
||||
Result2 = emqx_access_control:authenticate(Plain#{password => Jwt_Error}),
|
||||
ct:pal("Auth result for the invalid jwt: ~p~n", [Result2]),
|
||||
?assertEqual({error, invalid_signature}, Result2).
|
||||
|
||||
t_check_claims_clientid(_) ->
|
||||
application:set_env(emqx_auth_jwt, verify_claims, [{clientid, <<"%c">>}]),
|
||||
application:stop(emqx_auth_jwt), application:start(emqx_auth_jwt),
|
||||
Plain = #{clientid => <<"client23">>, username => <<"plain">>, zone => external},
|
||||
Jwt = sign([{clientid, <<"client23">>},
|
||||
{username, <<"plain">>},
|
||||
{exp, os:system_time(seconds) + 3}], <<"HS256">>, <<"emqxsecret">>),
|
||||
Result0 = emqx_access_control:authenticate(Plain#{password => Jwt}),
|
||||
ct:pal("Auth result: ~p~n", [Result0]),
|
||||
?assertMatch({ok, #{auth_result := success, jwt_claims := _}}, Result0),
|
||||
Jwt_Error = sign([{clientid, <<"client1">>},
|
||||
{username, <<"plain">>}], <<"HS256">>, <<"secret">>),
|
||||
Result2 = emqx_access_control:authenticate(Plain#{password => Jwt_Error}),
|
||||
ct:pal("Auth result for the invalid jwt: ~p~n", [Result2]),
|
||||
?assertEqual({error, invalid_signature}, Result2).
|
||||
|
||||
t_check_claims_username(_) ->
|
||||
application:set_env(emqx_auth_jwt, verify_claims, [{username, <<"%u">>}]),
|
||||
application:stop(emqx_auth_jwt), application:start(emqx_auth_jwt),
|
||||
|
||||
Plain = #{clientid => <<"client23">>, username => <<"plain">>, zone => external},
|
||||
Jwt = sign([{client_id, <<"client23">>},
|
||||
{username, <<"plain">>},
|
||||
{exp, os:system_time(seconds) + 3}], <<"HS256">>, <<"emqxsecret">>),
|
||||
Result0 = emqx_access_control:authenticate(Plain#{password => Jwt}),
|
||||
ct:pal("Auth result: ~p~n", [Result0]),
|
||||
?assertMatch({ok, #{auth_result := success, jwt_claims := _}}, Result0),
|
||||
Jwt_Error = sign([{clientid, <<"client1">>},
|
||||
{username, <<"plain">>}], <<"HS256">>, <<"secret">>),
|
||||
Result3 = emqx_access_control:authenticate(Plain#{password => Jwt_Error}),
|
||||
ct:pal("Auth result for the invalid jwt: ~p~n", [Result3]),
|
||||
?assertEqual({error, invalid_signature}, Result3).
|
||||
|
||||
t_check_claims_kid_in_header(_) ->
|
||||
application:set_env(emqx_auth_jwt, verify_claims, []),
|
||||
Plain = #{clientid => <<"client23">>, username => <<"plain">>, zone => external},
|
||||
Jwt = sign([{clientid, <<"client23">>},
|
||||
{username, <<"plain">>},
|
||||
{exp, os:system_time(seconds) + 3}],
|
||||
#{<<"alg">> => <<"HS256">>,
|
||||
<<"kid">> => <<"a_kid_str">>}, <<"emqxsecret">>),
|
||||
Result0 = emqx_access_control:authenticate(Plain#{password => Jwt}),
|
||||
ct:pal("Auth result: ~p~n", [Result0]),
|
||||
?assertMatch({ok, #{auth_result := success, jwt_claims := _}}, Result0).
|
|
@ -1,25 +0,0 @@
|
|||
.eunit
|
||||
deps
|
||||
*.o
|
||||
*.beam
|
||||
*.plt
|
||||
erl_crash.dump
|
||||
ebin
|
||||
rel/example_project
|
||||
.concrete/DEV_MODE
|
||||
.rebar
|
||||
.erlang.mk/
|
||||
emqx_auth_ldap.d
|
||||
data/
|
||||
cover/
|
||||
ct.coverdata
|
||||
eunit.coverdata
|
||||
logs/
|
||||
test/ct.cover.spec
|
||||
.DS_Store
|
||||
_build/
|
||||
rebar.lock
|
||||
erlang.mk
|
||||
rebar3.crashdump
|
||||
.rebar3/
|
||||
etc/emqx_auth_ldap.conf.rendered
|
|
@ -1,96 +0,0 @@
|
|||
emqx_auth_ldap
|
||||
==============
|
||||
|
||||
EMQ X LDAP Authentication Plugin
|
||||
|
||||
Build
|
||||
-----
|
||||
|
||||
```
|
||||
make
|
||||
```
|
||||
|
||||
Load the Plugin
|
||||
---------------
|
||||
|
||||
```
|
||||
# ./bin/emqx_ctl plugins load emqx_auth_ldap
|
||||
```
|
||||
|
||||
Generate Password
|
||||
---------------
|
||||
|
||||
```
|
||||
slappasswd -h '{ssha}' -s public
|
||||
```
|
||||
|
||||
Configuration Open LDAP
|
||||
-----------------------
|
||||
|
||||
vim /etc/openldap/slapd.conf
|
||||
|
||||
```
|
||||
include /etc/openldap/schema/core.schema
|
||||
include /etc/openldap/schema/cosine.schema
|
||||
include /etc/openldap/schema/inetorgperson.schema
|
||||
include /etc/openldap/schema/ppolicy.schema
|
||||
include /etc/openldap/schema/emqx.schema
|
||||
|
||||
database bdb
|
||||
suffix "dc=emqx,dc=io"
|
||||
rootdn "cn=root,dc=emqx,dc=io"
|
||||
rootpw {SSHA}eoF7NhNrejVYYyGHqnt+MdKNBh4r1w3W
|
||||
|
||||
directory /etc/openldap/data
|
||||
```
|
||||
|
||||
If the ldap launched with error below:
|
||||
```
|
||||
Unrecognized database type (bdb)
|
||||
5c4a72b9 slapd.conf: line 7: <database> failed init (bdb)
|
||||
slapadd: bad configuration file!
|
||||
```
|
||||
|
||||
Insert lines to the slapd.conf
|
||||
```
|
||||
modulepath /usr/lib/ldap
|
||||
moduleload back_bdb.la
|
||||
```
|
||||
|
||||
Import EMQX User Data
|
||||
----------------------
|
||||
|
||||
Use ldapadd
|
||||
```
|
||||
# ldapadd -x -D "cn=root,dc=emqx,dc=io" -w public -f emqx.com.ldif
|
||||
```
|
||||
|
||||
Use slapadd
|
||||
```
|
||||
# sudo slapadd -l schema/emqx.io.ldif -f slapd.conf
|
||||
```
|
||||
|
||||
Launch slapd
|
||||
```
|
||||
# sudo slapd -d 3
|
||||
```
|
||||
|
||||
Test
|
||||
-----
|
||||
After configure slapd correctly and launch slapd successfully.
|
||||
You could execute
|
||||
|
||||
``` bash
|
||||
# make tests
|
||||
```
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
Apache License Version 2.0
|
||||
|
||||
Author
|
||||
------
|
||||
|
||||
EMQ X Team.
|
||||
|
|
@ -1,135 +0,0 @@
|
|||
## create emqx.io
|
||||
|
||||
dn:dc=emqx,dc=io
|
||||
objectclass: top
|
||||
objectclass: dcobject
|
||||
objectclass: organization
|
||||
dc:emqx
|
||||
o:emqx,Inc.
|
||||
|
||||
# create testdevice.emqx.io
|
||||
dn:ou=testdevice,dc=emqx,dc=io
|
||||
objectClass: top
|
||||
objectclass:organizationalUnit
|
||||
ou:testdevice
|
||||
|
||||
# create user admin
|
||||
dn:uid=admin,ou=testdevice,dc=emqx,dc=io
|
||||
objectClass: top
|
||||
objectClass: simpleSecurityObject
|
||||
objectClass: account
|
||||
userPassword:: e1NIQX1XNnBoNU1tNVB6OEdnaVVMYlBnekczN21qOWc9
|
||||
uid: admin
|
||||
|
||||
## create user=mqttuser0001,
|
||||
# password=mqttuser0001,
|
||||
# passhash={SHA}mlb3fat40MKBTXUVZwCKmL73R/0=
|
||||
# base64passhash=e1NIQX1tbGIzZmF0NDBNS0JUWFVWWndDS21MNzNSLzA9
|
||||
dn:uid=mqttuser0001,ou=testdevice,dc=emqx,dc=io
|
||||
objectClass: top
|
||||
objectClass: mqttUser
|
||||
objectClass: mqttDevice
|
||||
objectClass: mqttSecurity
|
||||
uid: mqttuser0001
|
||||
isEnabled: TRUE
|
||||
mqttAccountName: user1
|
||||
mqttPublishTopic: mqttuser0001/pub/1
|
||||
mqttPublishTopic: mqttuser0001/pub/+
|
||||
mqttPublishTopic: mqttuser0001/pub/#
|
||||
mqttSubscriptionTopic: mqttuser0001/sub/1
|
||||
mqttSubscriptionTopic: mqttuser0001/sub/+
|
||||
mqttSubscriptionTopic: mqttuser0001/sub/#
|
||||
mqttPubSubTopic: mqttuser0001/pubsub/1
|
||||
mqttPubSubTopic: mqttuser0001/pubsub/+
|
||||
mqttPubSubTopic: mqttuser0001/pubsub/#
|
||||
userPassword:: e1NIQX1tbGIzZmF0NDBNS0JUWFVWWndDS21MNzNSLzA9
|
||||
|
||||
## create user=mqttuser0002
|
||||
# password=mqttuser0002,
|
||||
# passhash={SSHA}n9XdtoG4Q/TQ3TQF4Y+khJbMBH4qXj4M
|
||||
# base64passhash=e1NTSEF9bjlYZHRvRzRRL1RRM1RRRjRZK2toSmJNQkg0cVhqNE0=
|
||||
dn:uid=mqttuser0002,ou=testdevice,dc=emqx,dc=io
|
||||
objectClass: top
|
||||
objectClass: mqttUser
|
||||
objectClass: mqttDevice
|
||||
objectClass: mqttSecurity
|
||||
uid: mqttuser0002
|
||||
isEnabled: TRUE
|
||||
mqttAccountName: user2
|
||||
mqttPublishTopic: mqttuser0002/pub/1
|
||||
mqttPublishTopic: mqttuser0002/pub/+
|
||||
mqttPublishTopic: mqttuser0002/pub/#
|
||||
mqttSubscriptionTopic: mqttuser0002/sub/1
|
||||
mqttSubscriptionTopic: mqttuser0002/sub/+
|
||||
mqttSubscriptionTopic: mqttuser0002/sub/#
|
||||
mqttPubSubTopic: mqttuser0002/pubsub/1
|
||||
mqttPubSubTopic: mqttuser0002/pubsub/+
|
||||
mqttPubSubTopic: mqttuser0002/pubsub/#
|
||||
userPassword:: e1NTSEF9bjlYZHRvRzRRL1RRM1RRRjRZK2toSmJNQkg0cVhqNE0=
|
||||
|
||||
## create user mqttuser0003
|
||||
# password=mqttuser0003,
|
||||
# passhash={MD5}ybsPGoaK3nDyiQvveiCOIw==
|
||||
# base64passhash=e01ENX15YnNQR29hSzNuRHlpUXZ2ZWlDT0l3PT0=
|
||||
dn:uid=mqttuser0003,ou=testdevice,dc=emqx,dc=io
|
||||
objectClass: top
|
||||
objectClass: mqttUser
|
||||
objectClass: mqttDevice
|
||||
objectClass: mqttSecurity
|
||||
uid: mqttuser0003
|
||||
isEnabled: TRUE
|
||||
mqttPublishTopic: mqttuser0003/pub/1
|
||||
mqttPublishTopic: mqttuser0003/pub/+
|
||||
mqttPublishTopic: mqttuser0003/pub/#
|
||||
mqttSubscriptionTopic: mqttuser0003/sub/1
|
||||
mqttSubscriptionTopic: mqttuser0003/sub/+
|
||||
mqttSubscriptionTopic: mqttuser0003/sub/#
|
||||
mqttPubSubTopic: mqttuser0003/pubsub/1
|
||||
mqttPubSubTopic: mqttuser0003/pubsub/+
|
||||
mqttPubSubTopic: mqttuser0003/pubsub/#
|
||||
userPassword:: e01ENX15YnNQR29hSzNuRHlpUXZ2ZWlDT0l3PT0=
|
||||
|
||||
## create user mqttuser0004
|
||||
# password=mqttuser0004,
|
||||
# passhash={MD5}2Br6pPDSEDIEvUlu9+s+MA==
|
||||
# base64passhash=e01ENX0yQnI2cFBEU0VESUV2VWx1OStzK01BPT0=
|
||||
dn:uid=mqttuser0004,ou=testdevice,dc=emqx,dc=io
|
||||
objectClass: top
|
||||
objectClass: mqttUser
|
||||
objectClass: mqttDevice
|
||||
objectClass: mqttSecurity
|
||||
uid: mqttuser0004
|
||||
isEnabled: TRUE
|
||||
mqttPublishTopic: mqttuser0004/pub/1
|
||||
mqttPublishTopic: mqttuser0004/pub/+
|
||||
mqttPublishTopic: mqttuser0004/pub/#
|
||||
mqttSubscriptionTopic: mqttuser0004/sub/1
|
||||
mqttSubscriptionTopic: mqttuser0004/sub/+
|
||||
mqttSubscriptionTopic: mqttuser0004/sub/#
|
||||
mqttPubSubTopic: mqttuser0004/pubsub/1
|
||||
mqttPubSubTopic: mqttuser0004/pubsub/+
|
||||
mqttPubSubTopic: mqttuser0004/pubsub/#
|
||||
userPassword: {MD5}2Br6pPDSEDIEvUlu9+s+MA==
|
||||
|
||||
## create user mqttuser0005
|
||||
# password=mqttuser0005,
|
||||
# passhash={SHA}jKnxeEDGR14kE8AR7yuVFOelhz4=
|
||||
# base64passhash=e1NIQX1qS254ZUVER1IxNGtFOEFSN3l1VkZPZWxoejQ9
|
||||
objectClass: top
|
||||
dn:uid=mqttuser0005,ou=testdevice,dc=emqx,dc=io
|
||||
objectClass: mqttUser
|
||||
objectClass: mqttDevice
|
||||
objectClass: mqttSecurity
|
||||
uid: mqttuser0005
|
||||
isEnabled: TRUE
|
||||
mqttPublishTopic: mqttuser0005/pub/1
|
||||
mqttPublishTopic: mqttuser0005/pub/+
|
||||
mqttPublishTopic: mqttuser0005/pub/#
|
||||
mqttSubscriptionTopic: mqttuser0005/sub/1
|
||||
mqttSubscriptionTopic: mqttuser0005/sub/+
|
||||
mqttSubscriptionTopic: mqttuser0005/sub/#
|
||||
mqttPubSubTopic: mqttuser0005/pubsub/1
|
||||
mqttPubSubTopic: mqttuser0005/pubsub/+
|
||||
mqttPubSubTopic: mqttuser0005/pubsub/#
|
||||
userPassword: {SHA}jKnxeEDGR14kE8AR7yuVFOelhz4=
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
#
|
||||
# Preliminary Apple OS X Native LDAP Schema
|
||||
# This file is subject to change.
|
||||
#
|
||||
attributetype ( 1.3.6.1.4.1.11.2.53.2.2.3.1.2.3.1.3 NAME 'isEnabled'
|
||||
EQUALITY booleanMatch
|
||||
SYNTAX 1.3.6.1.4.1.1466.115.121.1.7
|
||||
SINGLE-VALUE
|
||||
USAGE userApplications )
|
||||
|
||||
attributetype ( 1.3.6.1.4.1.11.2.53.2.2.3.1.2.3.4.1 NAME ( 'mqttPublishTopic' 'mpt' )
|
||||
EQUALITY caseIgnoreMatch
|
||||
SUBSTR caseIgnoreSubstringsMatch
|
||||
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
|
||||
USAGE userApplications )
|
||||
attributetype ( 1.3.6.1.4.1.11.2.53.2.2.3.1.2.3.4.2 NAME ( 'mqttSubscriptionTopic' 'mst' )
|
||||
EQUALITY caseIgnoreMatch
|
||||
SUBSTR caseIgnoreSubstringsMatch
|
||||
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
|
||||
USAGE userApplications )
|
||||
attributetype ( 1.3.6.1.4.1.11.2.53.2.2.3.1.2.3.4.3 NAME ( 'mqttPubSubTopic' 'mpst' )
|
||||
EQUALITY caseIgnoreMatch
|
||||
SUBSTR caseIgnoreSubstringsMatch
|
||||
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
|
||||
USAGE userApplications )
|
||||
attributetype ( 1.3.6.1.4.1.11.2.53.2.2.3.1.2.3.4.4 NAME ( 'mqttAccountName' 'man' )
|
||||
EQUALITY caseIgnoreMatch
|
||||
SUBSTR caseIgnoreSubstringsMatch
|
||||
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
|
||||
USAGE userApplications )
|
||||
|
||||
|
||||
objectclass ( 1.3.6.1.4.1.11.2.53.2.2.3.1.2.3.4 NAME 'mqttUser'
|
||||
AUXILIARY
|
||||
MAY ( mqttPublishTopic $ mqttSubscriptionTopic $ mqttPubSubTopic $ mqttAccountName) )
|
||||
|
||||
objectclass ( 1.3.6.1.4.1.11.2.53.2.2.3.1.2.3.2 NAME 'mqttDevice'
|
||||
SUP top
|
||||
STRUCTURAL
|
||||
MUST ( uid )
|
||||
MAY ( isEnabled ) )
|
||||
|
||||
objectclass ( 1.3.6.1.4.1.11.2.53.2.2.3.1.2.3.3 NAME 'mqttSecurity'
|
||||
SUP top
|
||||
AUXILIARY
|
||||
MAY ( userPassword $ userPKCS12 $ pwdAttribute $ pwdLockout ) )
|
|
@ -1,76 +0,0 @@
|
|||
##--------------------------------------------------------------------
|
||||
## LDAP Auth Plugin
|
||||
##--------------------------------------------------------------------
|
||||
|
||||
## LDAP server list, seperated by ','.
|
||||
##
|
||||
## Value: String
|
||||
auth.ldap.servers = "127.0.0.1"
|
||||
|
||||
## LDAP server port.
|
||||
##
|
||||
## Value: Port
|
||||
auth.ldap.port = 389
|
||||
|
||||
## LDAP pool size
|
||||
##
|
||||
## Value: String
|
||||
auth.ldap.pool = 8
|
||||
|
||||
## LDAP Bind DN.
|
||||
##
|
||||
## Value: DN
|
||||
auth.ldap.bind_dn = "cn=root,dc=emqx,dc=io"
|
||||
|
||||
## LDAP Bind Password.
|
||||
##
|
||||
## Value: String
|
||||
auth.ldap.bind_password = public
|
||||
|
||||
## LDAP query timeout.
|
||||
##
|
||||
## Value: Number
|
||||
auth.ldap.timeout = 30s
|
||||
|
||||
## Device DN.
|
||||
##
|
||||
## Variables:
|
||||
##
|
||||
## Value: DN
|
||||
auth.ldap.device_dn = "ou=device,dc=emqx,dc=io"
|
||||
|
||||
## Specified ObjectClass
|
||||
##
|
||||
## Variables:
|
||||
##
|
||||
## Value: string
|
||||
auth.ldap.match_objectclass = mqttUser
|
||||
|
||||
## attributetype for username
|
||||
##
|
||||
## Variables:
|
||||
##
|
||||
## Value: string
|
||||
auth.ldap.username.attributetype = uid
|
||||
|
||||
## attributetype for password
|
||||
##
|
||||
## Variables:
|
||||
##
|
||||
## Value: string
|
||||
auth.ldap.password.attributetype = userPassword
|
||||
|
||||
## Whether to enable SSL.
|
||||
##
|
||||
## Value: true | false
|
||||
auth.ldap.ssl.enable = false
|
||||
|
||||
#auth.ldap.ssl.certfile = "etc/certs/cert.pem"
|
||||
|
||||
#auth.ldap.ssl.keyfile = "etc/certs/key.pem"
|
||||
|
||||
#auth.ldap.ssl.cacertfile = "etc/certs/cacert.pem"
|
||||
|
||||
#auth.ldap.ssl.verify = "verify_peer"
|
||||
|
||||
#auth.ldap.ssl.server_name_indication = your_server_name
|
|
@ -1,23 +0,0 @@
|
|||
|
||||
-define(APP, emqx_auth_ldap).
|
||||
|
||||
-record(auth_metrics, {
|
||||
success = 'client.auth.success',
|
||||
failure = 'client.auth.failure',
|
||||
ignore = 'client.auth.ignore'
|
||||
}).
|
||||
|
||||
-record(acl_metrics, {
|
||||
allow = 'client.acl.allow',
|
||||
deny = 'client.acl.deny',
|
||||
ignore = 'client.acl.ignore'
|
||||
}).
|
||||
|
||||
-define(METRICS(Type), tl(tuple_to_list(#Type{}))).
|
||||
-define(METRICS(Type, K), #Type{}#Type.K).
|
||||
|
||||
-define(AUTH_METRICS, ?METRICS(auth_metrics)).
|
||||
-define(AUTH_METRICS(K), ?METRICS(auth_metrics, K)).
|
||||
|
||||
-define(ACL_METRICS, ?METRICS(acl_metrics)).
|
||||
-define(ACL_METRICS(K), ?METRICS(acl_metrics, K)).
|
|
@ -1,174 +0,0 @@
|
|||
%%-*- mode: erlang -*-
|
||||
%% emqx_auth_ldap config mapping
|
||||
|
||||
{mapping, "auth.ldap.servers", "emqx_auth_ldap.ldap", [
|
||||
{default, "127.0.0.1"},
|
||||
{datatype, string}
|
||||
]}.
|
||||
|
||||
{mapping, "auth.ldap.port", "emqx_auth_ldap.ldap", [
|
||||
{default, 389},
|
||||
{datatype, integer}
|
||||
]}.
|
||||
|
||||
{mapping, "auth.ldap.pool", "emqx_auth_ldap.ldap", [
|
||||
{default, 8},
|
||||
{datatype, integer}
|
||||
]}.
|
||||
|
||||
{mapping, "auth.ldap.bind_dn", "emqx_auth_ldap.ldap", [
|
||||
{datatype, string},
|
||||
{default, "cn=root,dc=emqx,dc=io"}
|
||||
]}.
|
||||
|
||||
{mapping, "auth.ldap.bind_password", "emqx_auth_ldap.ldap", [
|
||||
{datatype, string},
|
||||
{default, "public"}
|
||||
]}.
|
||||
|
||||
{mapping, "auth.ldap.timeout", "emqx_auth_ldap.ldap", [
|
||||
{default, "30s"},
|
||||
{datatype, {duration, ms}}
|
||||
]}.
|
||||
|
||||
{mapping, "auth.ldap.ssl.enable", "emqx_auth_ldap.ldap", [
|
||||
{default, false},
|
||||
{datatype, {enum, [true, false]}}
|
||||
]}.
|
||||
|
||||
{mapping, "auth.ldap.ssl.certfile", "emqx_auth_ldap.ldap", [
|
||||
{datatype, string}
|
||||
]}.
|
||||
|
||||
{mapping, "auth.ldap.ssl.keyfile", "emqx_auth_ldap.ldap", [
|
||||
{datatype, string}
|
||||
]}.
|
||||
|
||||
{mapping, "auth.ldap.ssl.cacertfile", "emqx_auth_ldap.ldap", [
|
||||
{datatype, string}
|
||||
]}.
|
||||
|
||||
{mapping, "auth.ldap.ssl.verify", "emqx_auth_ldap.ldap", [
|
||||
{default, verify_none},
|
||||
{datatype, {enum, [verify_none, verify_peer]}}
|
||||
]}.
|
||||
|
||||
{mapping, "auth.ldap.ssl.server_name_indication", "emqx_auth_ldap.ldap", [
|
||||
{datatype, string}
|
||||
]}.
|
||||
|
||||
{translation, "emqx_auth_ldap.ldap", fun(Conf) ->
|
||||
A2N = fun(A) -> case inet:parse_address(A) of {ok, N} -> N; _ -> A end end,
|
||||
Servers = [A2N(A) || A <- string:tokens(cuttlefish:conf_get("auth.ldap.servers", Conf), ",")],
|
||||
Port = cuttlefish:conf_get("auth.ldap.port", Conf),
|
||||
Pool = cuttlefish:conf_get("auth.ldap.pool", Conf),
|
||||
BindDN = cuttlefish:conf_get("auth.ldap.bind_dn", Conf),
|
||||
BindPassword = cuttlefish:conf_get("auth.ldap.bind_password", Conf),
|
||||
Timeout = cuttlefish:conf_get("auth.ldap.timeout", Conf),
|
||||
Filter = fun(Ls) -> [E || E = {_, V} <- Ls, V /= undefined]end,
|
||||
SslOpts = fun() ->
|
||||
[{certfile, cuttlefish:conf_get("auth.ldap.ssl.certfile", Conf)},
|
||||
{keyfile, cuttlefish:conf_get("auth.ldap.ssl.keyfile", Conf)},
|
||||
{cacertfile, cuttlefish:conf_get("auth.ldap.ssl.cacertfile", Conf, undefined)},
|
||||
{verify, cuttlefish:conf_get("auth.ldap.ssl.verify", Conf, undefined)},
|
||||
{server_name_indication, case cuttlefish:conf_get("auth.ldap.ssl.server_name_indication", Conf, undefined) of
|
||||
"disable" -> disable;
|
||||
SNI -> SNI
|
||||
end}]
|
||||
end,
|
||||
Opts = [{servers, Servers},
|
||||
{port, Port},
|
||||
{timeout, Timeout},
|
||||
{bind_dn, BindDN},
|
||||
{bind_password, BindPassword},
|
||||
{pool, Pool},
|
||||
{auto_reconnect, 2}],
|
||||
case cuttlefish:conf_get("auth.ldap.ssl.enable", Conf) of
|
||||
true -> [{ssl, true}, {sslopts, Filter(SslOpts())}|Opts];
|
||||
false -> [{ssl, false}|Opts]
|
||||
end
|
||||
end}.
|
||||
|
||||
{mapping, "auth.ldap.device_dn", "emqx_auth_ldap.device_dn", [
|
||||
{default, "ou=device,dc=emqx,dc=io"},
|
||||
{datatype, string}
|
||||
]}.
|
||||
|
||||
{mapping, "auth.ldap.match_objectclass", "emqx_auth_ldap.match_objectclass", [
|
||||
{default, "mqttUser"},
|
||||
{datatype, string}
|
||||
]}.
|
||||
|
||||
{mapping, "auth.ldap.custom_base_dn", "emqx_auth_ldap.custom_base_dn", [
|
||||
{default, "${username_attr}=${user},${device_dn}"},
|
||||
{datatype, string}
|
||||
]}.
|
||||
|
||||
%% auth.ldap.filters.1.key = "objectClass"
|
||||
%% auth.ldap.filters.1.value = "mqttUser"
|
||||
%% auth.ldap.filters.1.op = "and"
|
||||
%% auth.ldap.filters.2.key = "uiAttr"
|
||||
%% auth.ldap.filters.2.value "someAttr"
|
||||
%% auth.ldap.filters.2.op = "or"
|
||||
%% auth.ldap.filters.3.key = "someKey"
|
||||
%% auth.ldap.filters.3.value = "someValue"
|
||||
%% The configuratation structure sent to the application:
|
||||
%% [{"objectClass","mqttUser"},"and",{"uiAttr","someAttr"},"or",{"someKey","someAttr"}]
|
||||
%% The resulting LDAP filter would look like this:
|
||||
%% ==> "|(&(objectClass=Class)(uiAttr=someAttr)(someKey=someValue))"
|
||||
{translation, "emqx_auth_ldap.filters",
|
||||
fun(Conf) ->
|
||||
Settings = cuttlefish_variable:filter_by_prefix("auth.ldap.filters", Conf),
|
||||
Keys = [{Num, {key, V}} || {["auth","ldap","filters", Num, "key"], V} <- Settings],
|
||||
Values = [{Num, {value, V}} || {["auth","ldap","filters", Num, "value"], V} <- Settings],
|
||||
Ops = [{Num, {op, V}} || {["auth","ldap","filters", Num, "op"], V} <- Settings],
|
||||
RawFilters = Keys ++ Values ++ Ops,
|
||||
Filters =
|
||||
lists:foldl(
|
||||
fun({Num,{T,V}}, Acc)->
|
||||
maps:update_with(Num,
|
||||
fun(F)->
|
||||
maps:put(T,V,F)
|
||||
end,
|
||||
#{T=>V}, Acc)
|
||||
end, #{}, RawFilters),
|
||||
Order=lists:usort(maps:keys(Filters)),
|
||||
lists:reverse(
|
||||
lists:foldl(
|
||||
fun(F,Acc)->
|
||||
case F of
|
||||
#{key:=K, op:=Op, value:=V} -> [Op,{K,V}|Acc];
|
||||
#{key:=K, value:=V} -> [{K,V}|Acc]
|
||||
end
|
||||
end,
|
||||
[],
|
||||
lists:map(fun(K) -> maps:get(K, Filters) end, Order)))
|
||||
end}.
|
||||
|
||||
{mapping, "auth.ldap.filters.$num.key", "emqx_auth_ldap.filters", [
|
||||
{datatype, string}
|
||||
]}.
|
||||
|
||||
{mapping, "auth.ldap.filters.$num.value", "emqx_auth_ldap.filters", [
|
||||
{datatype, string}
|
||||
]}.
|
||||
|
||||
{mapping, "auth.ldap.filters.$num.op", "emqx_auth_ldap.filters", [
|
||||
{datatype, {enum, [ "or", "and" ] } }
|
||||
]}.
|
||||
|
||||
|
||||
{mapping, "auth.ldap.bind_as_user", "emqx_auth_ldap.bind_as_user", [
|
||||
{default, false},
|
||||
{datatype, {enum, [true, false]}}
|
||||
]}.
|
||||
|
||||
{mapping, "auth.ldap.username.attributetype", "emqx_auth_ldap.username_attr", [
|
||||
{default, "uid"},
|
||||
{datatype, string}
|
||||
]}.
|
||||
|
||||
{mapping, "auth.ldap.password.attributetype", "emqx_auth_ldap.password_attr", [
|
||||
{default, "userPassword"},
|
||||
{datatype, string}
|
||||
]}.
|
|
@ -1,25 +0,0 @@
|
|||
{deps,
|
||||
[{eldap2, {git, "https://github.com/emqx/eldap2", {tag, "v0.2.2"}}}
|
||||
]}.
|
||||
|
||||
{profiles,
|
||||
[{test,
|
||||
[{deps, []}
|
||||
]}
|
||||
]}.
|
||||
|
||||
{edoc_opts, [{preprocess, true}]}.
|
||||
{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}.
|
||||
|
|
@ -1,98 +0,0 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_acl_ldap).
|
||||
|
||||
-include("emqx_auth_ldap.hrl").
|
||||
|
||||
-include_lib("emqx/include/emqx.hrl").
|
||||
-include_lib("eldap/include/eldap.hrl").
|
||||
-include_lib("emqx/include/logger.hrl").
|
||||
|
||||
-export([ register_metrics/0
|
||||
, check_acl/5
|
||||
, description/0
|
||||
]).
|
||||
|
||||
-import(proplists, [get_value/2]).
|
||||
|
||||
-import(emqx_auth_ldap_cli, [search/4]).
|
||||
|
||||
-spec(register_metrics() -> ok).
|
||||
register_metrics() ->
|
||||
lists:foreach(fun emqx_metrics:ensure/1, ?ACL_METRICS).
|
||||
|
||||
check_acl(ClientInfo, PubSub, Topic, NoMatchAction, State) ->
|
||||
case do_check_acl(ClientInfo, PubSub, Topic, NoMatchAction, State) of
|
||||
ok -> emqx_metrics:inc(?ACL_METRICS(ignore)), ok;
|
||||
{stop, allow} -> emqx_metrics:inc(?ACL_METRICS(allow)), {stop, allow};
|
||||
{stop, deny} -> emqx_metrics:inc(?ACL_METRICS(deny)), {stop, deny}
|
||||
end.
|
||||
|
||||
do_check_acl(#{username := <<$$, _/binary>>}, _PubSub, _Topic, _NoMatchAction, _State) ->
|
||||
ok;
|
||||
|
||||
do_check_acl(#{username := Username}, PubSub, Topic, _NoMatchAction,
|
||||
#{device_dn := DeviceDn,
|
||||
match_objectclass := ObjectClass,
|
||||
username_attr := UidAttr,
|
||||
custom_base_dn := CustomBaseDN,
|
||||
pool := Pool} = Config) ->
|
||||
|
||||
Filters = maps:get(filters, Config, []),
|
||||
|
||||
ReplaceRules = [{"${username_attr}", UidAttr},
|
||||
{"${user}", binary_to_list(Username)},
|
||||
{"${device_dn}", DeviceDn}],
|
||||
|
||||
Filter = emqx_auth_ldap:prepare_filter(Filters, UidAttr, ObjectClass, ReplaceRules),
|
||||
|
||||
Attribute = case PubSub of
|
||||
publish -> "mqttPublishTopic";
|
||||
subscribe -> "mqttSubscriptionTopic"
|
||||
end,
|
||||
Attribute1 = "mqttPubSubTopic",
|
||||
?LOG(debug, "[LDAP] search dn:~p filter:~p, attribute:~p",
|
||||
[DeviceDn, Filter, Attribute]),
|
||||
|
||||
BaseDN = emqx_auth_ldap:replace_vars(CustomBaseDN, ReplaceRules),
|
||||
|
||||
case search(Pool, BaseDN, Filter, [Attribute, Attribute1]) of
|
||||
{error, noSuchObject} ->
|
||||
ok;
|
||||
{ok, #eldap_search_result{entries = []}} ->
|
||||
ok;
|
||||
{ok, #eldap_search_result{entries = [Entry]}} ->
|
||||
Topics = get_value(Attribute, Entry#eldap_entry.attributes)
|
||||
++ get_value(Attribute1, Entry#eldap_entry.attributes),
|
||||
match(Topic, Topics);
|
||||
Error ->
|
||||
?LOG(error, "[LDAP] search error:~p", [Error]),
|
||||
{stop, deny}
|
||||
end.
|
||||
|
||||
match(_Topic, []) ->
|
||||
ok;
|
||||
|
||||
match(Topic, [Filter | Topics]) ->
|
||||
case emqx_topic:match(Topic, list_to_binary(Filter)) of
|
||||
true -> {stop, allow};
|
||||
false -> match(Topic, Topics)
|
||||
end.
|
||||
|
||||
description() ->
|
||||
"ACL with LDAP".
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
{application, emqx_auth_ldap,
|
||||
[{description, "EMQ X Authentication/ACL with LDAP"},
|
||||
{vsn, "4.4.0"}, % strict semver, bump manually!
|
||||
{modules, []},
|
||||
{registered, [emqx_auth_ldap_sup]},
|
||||
{applications, [kernel,stdlib,eldap2,ecpool]},
|
||||
{mod, {emqx_auth_ldap_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-ldap"}
|
||||
]}
|
||||
]}.
|
|
@ -1,210 +0,0 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_auth_ldap).
|
||||
|
||||
-include("emqx_auth_ldap.hrl").
|
||||
|
||||
-include_lib("emqx/include/emqx.hrl").
|
||||
-include_lib("eldap/include/eldap.hrl").
|
||||
-include_lib("emqx/include/logger.hrl").
|
||||
|
||||
-import(proplists, [get_value/2]).
|
||||
|
||||
-import(emqx_auth_ldap_cli, [search/3]).
|
||||
|
||||
-export([ register_metrics/0
|
||||
, check/3
|
||||
, description/0
|
||||
, prepare_filter/4
|
||||
, replace_vars/2
|
||||
]).
|
||||
|
||||
-spec(register_metrics() -> ok).
|
||||
register_metrics() ->
|
||||
lists:foreach(fun emqx_metrics:ensure/1, ?AUTH_METRICS).
|
||||
|
||||
check(ClientInfo = #{username := Username, password := Password}, AuthResult,
|
||||
State = #{password_attr := PasswdAttr, bind_as_user := BindAsUserRequired, pool := Pool}) ->
|
||||
CheckResult =
|
||||
case lookup_user(Username, State) of
|
||||
undefined -> {error, not_found};
|
||||
{error, Error} -> {error, Error};
|
||||
Entry ->
|
||||
PasswordString = binary_to_list(Password),
|
||||
ObjectName = Entry#eldap_entry.object_name,
|
||||
Attributes = Entry#eldap_entry.attributes,
|
||||
case BindAsUserRequired of
|
||||
true ->
|
||||
emqx_auth_ldap_cli:post_bind(Pool, ObjectName, PasswordString);
|
||||
false ->
|
||||
case get_value(PasswdAttr, Attributes) of
|
||||
undefined ->
|
||||
logger:error("LDAP Search State: ~p, uid: ~p, result:~p",
|
||||
[State, Username, Attributes]),
|
||||
{error, not_found};
|
||||
[Passhash1] ->
|
||||
format_password(Passhash1, Password, ClientInfo)
|
||||
end
|
||||
end
|
||||
end,
|
||||
case CheckResult of
|
||||
ok ->
|
||||
ok = emqx_metrics:inc(?AUTH_METRICS(success)),
|
||||
{stop, AuthResult#{auth_result => success, anonymous => false}};
|
||||
{error, not_found} ->
|
||||
emqx_metrics:inc(?AUTH_METRICS(ignore));
|
||||
{error, ResultCode} ->
|
||||
ok = emqx_metrics:inc(?AUTH_METRICS(failure)),
|
||||
?LOG(error, "[LDAP] Auth from ldap failed: ~p", [ResultCode]),
|
||||
{stop, AuthResult#{auth_result => ResultCode, anonymous => false}}
|
||||
end.
|
||||
|
||||
lookup_user(Username, #{username_attr := UidAttr,
|
||||
match_objectclass := ObjectClass,
|
||||
device_dn := DeviceDn,
|
||||
custom_base_dn := CustomBaseDN, pool := Pool} = Config) ->
|
||||
|
||||
Filters = maps:get(filters, Config, []),
|
||||
|
||||
ReplaceRules = [{"${username_attr}", UidAttr},
|
||||
{"${user}", binary_to_list(Username)},
|
||||
{"${device_dn}", DeviceDn}],
|
||||
|
||||
Filter = prepare_filter(Filters, UidAttr, ObjectClass, ReplaceRules),
|
||||
|
||||
%% auth.ldap.custom_base_dn = "${username_attr}=${user},${device_dn}"
|
||||
BaseDN = replace_vars(CustomBaseDN, ReplaceRules),
|
||||
|
||||
case search(Pool, BaseDN, Filter) of
|
||||
%% This clause seems to be impossible to match. `eldap2:search/2` does
|
||||
%% not validates the result, so if it returns "successfully" from the
|
||||
%% LDAP server, it always returns `{ok, #eldap_search_result{}}`.
|
||||
{error, noSuchObject} ->
|
||||
undefined;
|
||||
%% In case no user was found by the search, but the search was completed
|
||||
%% without error we get an empty `entries` list.
|
||||
{ok, #eldap_search_result{entries = []}} ->
|
||||
undefined;
|
||||
{ok, #eldap_search_result{entries = [Entry]}} ->
|
||||
Attributes = Entry#eldap_entry.attributes,
|
||||
case get_value("isEnabled", Attributes) of
|
||||
undefined ->
|
||||
Entry;
|
||||
[Val] ->
|
||||
case list_to_atom(string:to_lower(Val)) of
|
||||
true -> Entry;
|
||||
false -> {error, username_disabled}
|
||||
end
|
||||
end;
|
||||
{error, Error} ->
|
||||
?LOG(error, "[LDAP] Search dn: ~p, filter: ~p, fail:~p", [DeviceDn, Filter, Error]),
|
||||
{error, username_or_password_error}
|
||||
end.
|
||||
|
||||
check_pass(Password, Password, _ClientInfo) -> ok;
|
||||
check_pass(_, _, _) -> {error, bad_username_or_password}.
|
||||
|
||||
format_password(Passhash, Password, ClientInfo) ->
|
||||
case do_format_password(Passhash, Password) of
|
||||
{error, Error2} ->
|
||||
{error, Error2};
|
||||
{Passhash1, Password1} ->
|
||||
check_pass(Passhash1, Password1, ClientInfo)
|
||||
end.
|
||||
|
||||
do_format_password(Passhash, Password) ->
|
||||
Base64PasshashHandler =
|
||||
handle_passhash(fun(HashType, Passhash1, Password1) ->
|
||||
Passhash2 = binary_to_list(base64:decode(Passhash1)),
|
||||
resolve_passhash(HashType, Passhash2, Password1)
|
||||
end,
|
||||
fun(_Passhash, _Password) ->
|
||||
{error, password_error}
|
||||
end),
|
||||
PasshashHandler = handle_passhash(fun resolve_passhash/3, Base64PasshashHandler),
|
||||
PasshashHandler(Passhash, Password).
|
||||
|
||||
resolve_passhash(HashType, Passhash, Password) ->
|
||||
[_, Passhash1] = string:tokens(Passhash, "}"),
|
||||
do_resolve(HashType, Passhash1, Password).
|
||||
|
||||
handle_passhash(HandleMatch, HandleNoMatch) ->
|
||||
fun(Passhash, Password) ->
|
||||
case re:run(Passhash, "(?<={)[^{}]+(?=})", [{capture, all, list}, global]) of
|
||||
{match, [[HashType]]} ->
|
||||
HandleMatch(list_to_atom(string:to_lower(HashType)), Passhash, Password);
|
||||
_ ->
|
||||
HandleNoMatch(Passhash, Password)
|
||||
end
|
||||
end.
|
||||
|
||||
do_resolve(ssha, Passhash, Password) ->
|
||||
D64 = base64:decode(Passhash),
|
||||
{HashedData, Salt} = lists:split(20, binary_to_list(D64)),
|
||||
NewHash = crypto:hash(sha, <<Password/binary, (list_to_binary(Salt))/binary>>),
|
||||
{list_to_binary(HashedData), NewHash};
|
||||
do_resolve(HashType, Passhash, Password) ->
|
||||
Password1 = base64:encode(crypto:hash(HashType, Password)),
|
||||
{list_to_binary(Passhash), Password1}.
|
||||
|
||||
description() -> "LDAP Authentication Plugin".
|
||||
|
||||
prepare_filter(Filters, _UidAttr, ObjectClass, ReplaceRules) ->
|
||||
SubFilters =
|
||||
lists:map(fun({K, V}) ->
|
||||
{replace_vars(K, ReplaceRules), replace_vars(V, ReplaceRules)};
|
||||
(Op) ->
|
||||
Op
|
||||
end, Filters),
|
||||
case SubFilters of
|
||||
[] -> eldap2:equalityMatch("objectClass", ObjectClass);
|
||||
_List -> compile_filters(SubFilters, [])
|
||||
end.
|
||||
|
||||
|
||||
compile_filters([{Key, Value}], []) ->
|
||||
compile_equal(Key, Value);
|
||||
compile_filters([{K1, V1}, "and", {K2, V2} | Rest], []) ->
|
||||
compile_filters(
|
||||
Rest,
|
||||
eldap2:'and'([compile_equal(K1, V1),
|
||||
compile_equal(K2, V2)]));
|
||||
compile_filters([{K1, V1}, "or", {K2, V2} | Rest], []) ->
|
||||
compile_filters(
|
||||
Rest,
|
||||
eldap2:'or'([compile_equal(K1, V1),
|
||||
compile_equal(K2, V2)]));
|
||||
compile_filters(["and", {K, V} | Rest], PartialFilter) ->
|
||||
compile_filters(
|
||||
Rest,
|
||||
eldap2:'and'([PartialFilter,
|
||||
compile_equal(K, V)]));
|
||||
compile_filters(["or", {K, V} | Rest], PartialFilter) ->
|
||||
compile_filters(
|
||||
Rest,
|
||||
eldap2:'or'([PartialFilter,
|
||||
compile_equal(K, V)]));
|
||||
compile_filters([], Filter) ->
|
||||
Filter.
|
||||
|
||||
compile_equal(Key, Value) ->
|
||||
eldap2:equalityMatch(Key, Value).
|
||||
|
||||
replace_vars(CustomBaseDN, ReplaceRules) ->
|
||||
lists:foldl(fun({Pattern, Substitute}, DN) ->
|
||||
lists:flatten(string:replace(DN, Pattern, Substitute))
|
||||
end, CustomBaseDN, ReplaceRules).
|
|
@ -1,78 +0,0 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_auth_ldap_app).
|
||||
|
||||
-behaviour(application).
|
||||
|
||||
-emqx_plugin(auth).
|
||||
|
||||
-include("emqx_auth_ldap.hrl").
|
||||
|
||||
%% Application callbacks
|
||||
-export([ start/2
|
||||
, prep_stop/1
|
||||
, stop/1
|
||||
]).
|
||||
|
||||
start(_StartType, _StartArgs) ->
|
||||
{ok, Sup} = emqx_auth_ldap_sup:start_link(),
|
||||
_ = if_enabled([device_dn, match_objectclass,
|
||||
username_attr, password_attr,
|
||||
filters, custom_base_dn, bind_as_user],
|
||||
fun load_auth_hook/1),
|
||||
_ = if_enabled([device_dn, match_objectclass,
|
||||
username_attr, password_attr,
|
||||
filters, custom_base_dn, bind_as_user],
|
||||
fun load_acl_hook/1),
|
||||
{ok, Sup}.
|
||||
|
||||
prep_stop(State) ->
|
||||
emqx:unhook('client.authenticate',{emqx_auth_ldap, check}),
|
||||
emqx:unhook('client.check_acl', {emqx_acl_ldap, check_acl}),
|
||||
State.
|
||||
|
||||
stop(_State) ->
|
||||
ok.
|
||||
|
||||
load_auth_hook(DeviceDn) ->
|
||||
ok = emqx_auth_ldap:register_metrics(),
|
||||
Params = maps:from_list(DeviceDn),
|
||||
emqx:hook('client.authenticate', {emqx_auth_ldap, check, [Params#{pool => ?APP}]}).
|
||||
|
||||
load_acl_hook(DeviceDn) ->
|
||||
ok = emqx_acl_ldap:register_metrics(),
|
||||
Params = maps:from_list(DeviceDn),
|
||||
emqx:hook('client.check_acl', {emqx_acl_ldap, check_acl, [Params#{pool => ?APP}]}).
|
||||
|
||||
if_enabled(Cfgs, Fun) ->
|
||||
case get_env(Cfgs) of
|
||||
{ok, []} -> ok;
|
||||
{ok, InitArgs} -> Fun(InitArgs)
|
||||
end.
|
||||
|
||||
get_env(Cfgs) ->
|
||||
get_env(Cfgs, []).
|
||||
|
||||
get_env([Cfg | LeftCfgs], ENVS) ->
|
||||
case application:get_env(?APP, Cfg) of
|
||||
{ok, ENV} ->
|
||||
get_env(LeftCfgs, [{Cfg, ENV} | ENVS]);
|
||||
undefined ->
|
||||
get_env(LeftCfgs, ENVS)
|
||||
end;
|
||||
get_env([], ENVS) ->
|
||||
{ok, ENVS}.
|
|
@ -1,150 +0,0 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_auth_ldap_cli).
|
||||
|
||||
-behaviour(ecpool_worker).
|
||||
|
||||
-include("emqx_auth_ldap.hrl").
|
||||
|
||||
-include_lib("emqx/include/emqx.hrl").
|
||||
-include_lib("emqx/include/logger.hrl").
|
||||
|
||||
%% ecpool callback
|
||||
-export([connect/1]).
|
||||
|
||||
-export([ search/3
|
||||
, search/4
|
||||
, post_bind/3
|
||||
, init_args/1
|
||||
]).
|
||||
|
||||
-import(proplists,
|
||||
[ get_value/2
|
||||
, get_value/3
|
||||
]).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% LDAP Connect/Search
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
connect(Opts) ->
|
||||
Servers = get_value(servers, Opts, ["localhost"]),
|
||||
Port = get_value(port, Opts, 389),
|
||||
Timeout = get_value(timeout, Opts, 30),
|
||||
BindDn = get_value(bind_dn, Opts),
|
||||
BindPassword = get_value(bind_password, Opts),
|
||||
LdapOpts = case get_value(ssl, Opts, false)of
|
||||
true ->
|
||||
SslOpts = get_value(sslopts, Opts),
|
||||
[{port, Port}, {timeout, Timeout}, {sslopts, SslOpts}];
|
||||
false ->
|
||||
[{port, Port}, {timeout, Timeout}]
|
||||
end,
|
||||
?LOG(debug, "[LDAP] Connecting to OpenLDAP server: ~p, Opts:~p ...", [Servers, LdapOpts]),
|
||||
|
||||
case eldap2:open(Servers, LdapOpts) of
|
||||
{ok, LDAP} ->
|
||||
try eldap2:simple_bind(LDAP, BindDn, BindPassword) of
|
||||
ok -> {ok, LDAP};
|
||||
{error, Error} ->
|
||||
?LOG(error, "[LDAP] Can't authenticated to OpenLDAP server: ~p", [Error]),
|
||||
{error, Error}
|
||||
catch
|
||||
error:Reason ->
|
||||
?LOG(error, "[LDAP] Can't authenticated to OpenLDAP server: ~p", [Reason]),
|
||||
{error, Reason}
|
||||
end;
|
||||
{error, Reason} ->
|
||||
?LOG(error, "[LDAP] Can't connect to OpenLDAP server: ~p", [Reason]),
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
search(Pool, Base, Filter) ->
|
||||
ecpool:with_client(Pool,
|
||||
fun(C) ->
|
||||
case application:get_env(?APP, bind_as_user) of
|
||||
{ok, true} ->
|
||||
{ok, Opts} = application:get_env(?APP, ldap),
|
||||
BindDn = get_value(bind_dn, Opts),
|
||||
BindPassword = get_value(bind_password, Opts),
|
||||
try eldap2:simple_bind(C, BindDn, BindPassword) of
|
||||
ok ->
|
||||
eldap2:search(C, [{base, Base},
|
||||
{filter, Filter},
|
||||
{deref, eldap2:derefFindingBaseObj()}]);
|
||||
{error, Error} ->
|
||||
{error, Error}
|
||||
catch
|
||||
error:Reason -> {error, Reason}
|
||||
end;
|
||||
{ok, false} ->
|
||||
eldap2:search(C, [{base, Base},
|
||||
{filter, Filter},
|
||||
{deref, eldap2:derefFindingBaseObj()}])
|
||||
end
|
||||
end).
|
||||
|
||||
search(Pool, Base, Filter, Attributes) ->
|
||||
ecpool:with_client(Pool,
|
||||
fun(C) ->
|
||||
case application:get_env(?APP, bind_as_user) of
|
||||
{ok, true} ->
|
||||
{ok, Opts} = application:get_env(?APP, ldap),
|
||||
BindDn = get_value(bind_dn, Opts),
|
||||
BindPassword = get_value(bind_password, Opts),
|
||||
try eldap2:simple_bind(C, BindDn, BindPassword) of
|
||||
ok ->
|
||||
eldap2:search(C, [{base, Base},
|
||||
{filter, Filter},
|
||||
{attributes, Attributes},
|
||||
{deref, eldap2:derefFindingBaseObj()}]);
|
||||
{error, Error} ->
|
||||
{error, Error}
|
||||
catch
|
||||
error:Reason -> {error, Reason}
|
||||
end;
|
||||
{ok, false} ->
|
||||
eldap2:search(C, [{base, Base},
|
||||
{filter, Filter},
|
||||
{attributes, Attributes},
|
||||
{deref, eldap2:derefFindingBaseObj()}])
|
||||
end
|
||||
end).
|
||||
|
||||
post_bind(Pool, BindDn, BindPassword) ->
|
||||
ecpool:with_client(Pool,
|
||||
fun(C) ->
|
||||
try eldap2:simple_bind(C, BindDn, BindPassword) of
|
||||
ok -> ok;
|
||||
{error, Error} ->
|
||||
{error, Error}
|
||||
catch
|
||||
error:Reason -> {error, Reason}
|
||||
end
|
||||
end).
|
||||
|
||||
|
||||
init_args(ENVS) ->
|
||||
DeviceDn = get_value(device_dn, ENVS),
|
||||
ObjectClass = get_value(match_objectclass, ENVS),
|
||||
UidAttr = get_value(username_attr, ENVS),
|
||||
PasswdAttr = get_value(password_attr, ENVS),
|
||||
{ok, #{device_dn => DeviceDn,
|
||||
match_objectclass => ObjectClass,
|
||||
username_attr => UidAttr,
|
||||
password_attr => PasswdAttr}}.
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_auth_ldap_sup).
|
||||
|
||||
-behaviour(supervisor).
|
||||
|
||||
-include("emqx_auth_ldap.hrl").
|
||||
|
||||
-export([start_link/0]).
|
||||
|
||||
-export([init/1]).
|
||||
|
||||
start_link() ->
|
||||
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
|
||||
|
||||
init([]) ->
|
||||
%% LDAP Connection Pool.
|
||||
{ok, Server} = application:get_env(?APP, ldap),
|
||||
PoolSpec = ecpool:pool_spec(?APP, ?APP, emqx_auth_ldap_cli, Server),
|
||||
{ok, {{one_for_one, 10, 100}, [PoolSpec]}}.
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIDUTCCAjmgAwIBAgIJAPPYCjTmxdt/MA0GCSqGSIb3DQEBCwUAMD8xCzAJBgNV
|
||||
BAYTAkNOMREwDwYDVQQIDAhoYW5nemhvdTEMMAoGA1UECgwDRU1RMQ8wDQYDVQQD
|
||||
DAZSb290Q0EwHhcNMjAwNTA4MDgwNjUyWhcNMzAwNTA2MDgwNjUyWjA/MQswCQYD
|
||||
VQQGEwJDTjERMA8GA1UECAwIaGFuZ3pob3UxDDAKBgNVBAoMA0VNUTEPMA0GA1UE
|
||||
AwwGUm9vdENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzcgVLex1
|
||||
EZ9ON64EX8v+wcSjzOZpiEOsAOuSXOEN3wb8FKUxCdsGrsJYB7a5VM/Jot25Mod2
|
||||
juS3OBMg6r85k2TWjdxUoUs+HiUB/pP/ARaaW6VntpAEokpij/przWMPgJnBF3Ur
|
||||
MjtbLayH9hGmpQrI5c2vmHQ2reRZnSFbY+2b8SXZ+3lZZgz9+BaQYWdQWfaUWEHZ
|
||||
uDaNiViVO0OT8DRjCuiDp3yYDj3iLWbTA/gDL6Tf5XuHuEwcOQUrd+h0hyIphO8D
|
||||
tsrsHZ14j4AWYLk1CPA6pq1HIUvEl2rANx2lVUNv+nt64K/Mr3RnVQd9s8bK+TXQ
|
||||
KGHd2Lv/PALYuwIDAQABo1AwTjAdBgNVHQ4EFgQUGBmW+iDzxctWAWxmhgdlE8Pj
|
||||
EbQwHwYDVR0jBBgwFoAUGBmW+iDzxctWAWxmhgdlE8PjEbQwDAYDVR0TBAUwAwEB
|
||||
/zANBgkqhkiG9w0BAQsFAAOCAQEAGbhRUjpIred4cFAFJ7bbYD9hKu/yzWPWkMRa
|
||||
ErlCKHmuYsYk+5d16JQhJaFy6MGXfLgo3KV2itl0d+OWNH0U9ULXcglTxy6+njo5
|
||||
CFqdUBPwN1jxhzo9yteDMKF4+AHIxbvCAJa17qcwUKR5MKNvv09C6pvQDJLzid7y
|
||||
E2dkgSuggik3oa0427KvctFf8uhOV94RvEDyqvT5+pgNYZ2Yfga9pD/jjpoHEUlo
|
||||
88IGU8/wJCx3Ds2yc8+oBg/ynxG8f/HmCC1ET6EHHoe2jlo8FpU/SgGtghS1YL30
|
||||
IWxNsPrUP+XsZpBJy/mvOhE5QXo6Y35zDqqj8tI7AGmAWu22jg==
|
||||
-----END CERTIFICATE-----
|
|
@ -1,19 +0,0 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIDEzCCAfugAwIBAgIBAjANBgkqhkiG9w0BAQsFADA/MQswCQYDVQQGEwJDTjER
|
||||
MA8GA1UECAwIaGFuZ3pob3UxDDAKBgNVBAoMA0VNUTEPMA0GA1UEAwwGUm9vdENB
|
||||
MB4XDTIwMDUwODA4MDcwNVoXDTMwMDUwNjA4MDcwNVowPzELMAkGA1UEBhMCQ04x
|
||||
ETAPBgNVBAgMCGhhbmd6aG91MQwwCgYDVQQKDANFTVExDzANBgNVBAMMBlNlcnZl
|
||||
cjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALNeWT3pE+QFfiRJzKmn
|
||||
AMUrWo3K2j/Tm3+Xnl6WLz67/0rcYrJbbKvS3uyRP/stXyXEKw9CepyQ1ViBVFkW
|
||||
Aoy8qQEOWFDsZc/5UzhXUnb6LXr3qTkFEjNmhj+7uzv/lbBxlUG1NlYzSeOB6/RT
|
||||
8zH/lhOeKhLnWYPXdXKsa1FL6ij4X8DeDO1kY7fvAGmBn/THh1uTpDizM4YmeI+7
|
||||
4dmayA5xXvARte5h4Vu5SIze7iC057N+vymToMk2Jgk+ZZFpyXrnq+yo6RaD3ANc
|
||||
lrc4FbeUQZ5a5s5Sxgs9a0Y3WMG+7c5VnVXcbjBRz/aq2NtOnQQjikKKQA8GF080
|
||||
BQkCAwEAAaMaMBgwCQYDVR0TBAIwADALBgNVHQ8EBAMCBeAwDQYJKoZIhvcNAQEL
|
||||
BQADggEBAJefnMZpaRDHQSNUIEL3iwGXE9c6PmIsQVE2ustr+CakBp3TZ4l0enLt
|
||||
iGMfEVFju69cO4oyokWv+hl5eCMkHBf14Kv51vj448jowYnF1zmzn7SEzm5Uzlsa
|
||||
sqjtAprnLyof69WtLU1j5rYWBuFX86yOTwRAFNjm9fvhAcrEONBsQtqipBWkMROp
|
||||
iUYMkRqbKcQMdwxov+lHBYKq9zbWRoqLROAn54SRqgQk6c15JdEfgOOjShbsOkIH
|
||||
UhqcwRkQic7n1zwHVGVDgNIZVgmJ2IdIWBlPEC7oLrRrBD/X1iEEXtKab6p5o22n
|
||||
KB5mN+iQaE+Oe2cpGKZJiJRdM+IqDDQ=
|
||||
-----END CERTIFICATE-----
|
|
@ -1,19 +0,0 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIDEzCCAfugAwIBAgIBATANBgkqhkiG9w0BAQsFADA/MQswCQYDVQQGEwJDTjER
|
||||
MA8GA1UECAwIaGFuZ3pob3UxDDAKBgNVBAoMA0VNUTEPMA0GA1UEAwwGUm9vdENB
|
||||
MB4XDTIwMDUwODA4MDY1N1oXDTMwMDUwNjA4MDY1N1owPzELMAkGA1UEBhMCQ04x
|
||||
ETAPBgNVBAgMCGhhbmd6aG91MQwwCgYDVQQKDANFTVExDzANBgNVBAMMBkNsaWVu
|
||||
dDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMy4hoksKcZBDbY680u6
|
||||
TS25U51nuB1FBcGMlF9B/t057wPOlxF/OcmbxY5MwepS41JDGPgulE1V7fpsXkiW
|
||||
1LUimYV/tsqBfymIe0mlY7oORahKji7zKQ2UBIVFhdlvQxunlIDnw6F9popUgyHt
|
||||
dMhtlgZK8oqRwHxO5dbfoukYd6J/r+etS5q26sgVkf3C6dt0Td7B25H9qW+f7oLV
|
||||
PbcHYCa+i73u9670nrpXsC+Qc7Mygwa2Kq/jwU+ftyLQnOeW07DuzOwsziC/fQZa
|
||||
nbxR+8U9FNftgRcC3uP/JMKYUqsiRAuaDokARZxVTV5hUElfpO6z6/NItSDvvh3i
|
||||
eikCAwEAAaMaMBgwCQYDVR0TBAIwADALBgNVHQ8EBAMCBeAwDQYJKoZIhvcNAQEL
|
||||
BQADggEBABchYxKo0YMma7g1qDswJXsR5s56Czx/I+B41YcpMBMTrRqpUC0nHtLk
|
||||
M7/tZp592u/tT8gzEnQjZLKBAhFeZaR3aaKyknLqwiPqJIgg0pgsBGITrAK3Pv4z
|
||||
5/YvAJJKgTe5UdeTz6U4lvNEux/4juZ4pmqH4qSFJTOzQS7LmgSmNIdd072rwXBd
|
||||
UzcSHzsJgEMb88u/LDLjj1pQ7AtZ4Tta8JZTvcgBFmjB0QUi6fgkHY6oGat/W4kR
|
||||
jSRUBlMUbM/drr2PVzRc2dwbFIl3X+ZE6n5Sl3ZwRAC/s92JU6CPMRW02muVu6xl
|
||||
goraNgPISnrbpR6KjxLZkVembXzjNNc=
|
||||
-----END CERTIFICATE-----
|
|
@ -1,27 +0,0 @@
|
|||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEpAIBAAKCAQEAzLiGiSwpxkENtjrzS7pNLblTnWe4HUUFwYyUX0H+3TnvA86X
|
||||
EX85yZvFjkzB6lLjUkMY+C6UTVXt+mxeSJbUtSKZhX+2yoF/KYh7SaVjug5FqEqO
|
||||
LvMpDZQEhUWF2W9DG6eUgOfDoX2milSDIe10yG2WBkryipHAfE7l1t+i6Rh3on+v
|
||||
561LmrbqyBWR/cLp23RN3sHbkf2pb5/ugtU9twdgJr6Lve73rvSeulewL5BzszKD
|
||||
BrYqr+PBT5+3ItCc55bTsO7M7CzOIL99BlqdvFH7xT0U1+2BFwLe4/8kwphSqyJE
|
||||
C5oOiQBFnFVNXmFQSV+k7rPr80i1IO++HeJ6KQIDAQABAoIBAGWgvPjfuaU3qizq
|
||||
uti/FY07USz0zkuJdkANH6LiSjlchzDmn8wJ0pApCjuIE0PV/g9aS8z4opp5q/gD
|
||||
UBLM/a8mC/xf2EhTXOMrY7i9p/I3H5FZ4ZehEqIw9sWKK9YzC6dw26HabB2BGOnW
|
||||
5nozPSQ6cp2RGzJ7BIkxSZwPzPnVTgy3OAuPOiJytvK+hGLhsNaT+Y9bNDvplVT2
|
||||
ZwYTV8GlHZC+4b2wNROILm0O86v96O+Qd8nn3fXjGHbMsAnONBq10bZS16L4fvkH
|
||||
5G+W/1PeSXmtZFppdRRDxIW+DWcXK0D48WRliuxcV4eOOxI+a9N2ZJZZiNLQZGwg
|
||||
w3A8+mECgYEA8HuJFrlRvdoBe2U/EwUtG74dcyy30L4yEBnN5QscXmEEikhaQCfX
|
||||
Wm6EieMcIB/5I5TQmSw0cmBMeZjSXYoFdoI16/X6yMMuATdxpvhOZGdUGXxhAH+x
|
||||
xoTUavWZnEqW3fkUU71kT5E2f2i+0zoatFESXHeslJyz85aAYpP92H0CgYEA2e5A
|
||||
Yozt5eaA1Gyhd8SeptkEU4xPirNUnVQHStpMWUb1kzTNXrPmNWccQ7JpfpG6DcYl
|
||||
zUF6p6mlzY+zkMiyPQjwEJlhiHM2NlL1QS7td0R8ewgsFoyn8WsBI4RejWrEG9td
|
||||
EDniuIw+pBFkcWthnTLHwECHdzgquToyTMjrBB0CgYEA28tdGbrZXhcyAZEhHAZA
|
||||
Gzog+pKlkpEzeonLKIuGKzCrEKRecIK5jrqyQsCjhS0T7ZRnL4g6i0s+umiV5M5w
|
||||
fcc292pEA1h45L3DD6OlKplSQVTv55/OYS4oY3YEJtf5mfm8vWi9lQeY8sxOlQpn
|
||||
O+VZTdBHmTC8PGeTAgZXHZUCgYA6Tyv88lYowB7SN2qQgBQu8jvdGtqhcs/99GCr
|
||||
H3N0I69LPsKAR0QeH8OJPXBKhDUywESXAaEOwS5yrLNP1tMRz5Vj65YUCzeDG3kx
|
||||
gpvY4IMp7ArX0bSRvJ6mYSFnVxy3k174G3TVCfksrtagHioVBGQ7xUg5ltafjrms
|
||||
n8l55QKBgQDVzU8tQvBVqY8/1lnw11Vj4fkE/drZHJ5UkdC1eenOfSWhlSLfUJ8j
|
||||
ds7vEWpRPPoVuPZYeR1y78cyxKe1GBx6Wa2lF5c7xjmiu0xbRnrxYeLolce9/ntp
|
||||
asClqpnHT8/VJYTD7Kqj0fouTTZf0zkig/y+2XERppd8k+pSKjUCPQ==
|
||||
-----END RSA PRIVATE KEY-----
|
|
@ -1,27 +0,0 @@
|
|||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEowIBAAKCAQEAs15ZPekT5AV+JEnMqacAxStajcraP9Obf5eeXpYvPrv/Stxi
|
||||
sltsq9Le7JE/+y1fJcQrD0J6nJDVWIFUWRYCjLypAQ5YUOxlz/lTOFdSdvotevep
|
||||
OQUSM2aGP7u7O/+VsHGVQbU2VjNJ44Hr9FPzMf+WE54qEudZg9d1cqxrUUvqKPhf
|
||||
wN4M7WRjt+8AaYGf9MeHW5OkOLMzhiZ4j7vh2ZrIDnFe8BG17mHhW7lIjN7uILTn
|
||||
s36/KZOgyTYmCT5lkWnJeuer7KjpFoPcA1yWtzgVt5RBnlrmzlLGCz1rRjdYwb7t
|
||||
zlWdVdxuMFHP9qrY206dBCOKQopADwYXTzQFCQIDAQABAoIBAQCuvCbr7Pd3lvI/
|
||||
n7VFQG+7pHRe1VKwAxDkx2t8cYos7y/QWcm8Ptwqtw58HzPZGWYrgGMCRpzzkRSF
|
||||
V9g3wP1S5Scu5C6dBu5YIGc157tqNGXB+SpdZddJQ4Nc6yGHXYERllT04ffBGc3N
|
||||
WG/oYS/1cSteiSIrsDy/91FvGRCi7FPxH3wIgHssY/tw69s1Cfvaq5lr2NTFzxIG
|
||||
xCvpJKEdSfVfS9I7LYiymVjst3IOR/w76/ZFY9cRa8ZtmQSWWsm0TUpRC1jdcbkm
|
||||
ZoJptYWlP+gSwx/fpMYftrkJFGOJhHJHQhwxT5X/ajAISeqjjwkWSEJLwnHQd11C
|
||||
Zy2+29lBAoGBANlEAIK4VxCqyPXNKfoOOi5dS64NfvyH4A1v2+KaHWc7lqaqPN49
|
||||
ezfN2n3X+KWx4cviDD914Yc2JQ1vVJjSaHci7yivocDo2OfZDmjBqzaMp/y+rX1R
|
||||
/f3MmiTqMa468rjaxI9RRZu7vDgpTR+za1+OBCgMzjvAng8dJuN/5gjlAoGBANNY
|
||||
uYPKtearBmkqdrSV7eTUe49Nhr0XotLaVBH37TCW0Xv9wjO2xmbm5Ga/DCtPIsBb
|
||||
yPeYwX9FjoasuadUD7hRvbFu6dBa0HGLmkXRJZTcD7MEX2Lhu4BuC72yDLLFd0r+
|
||||
Ep9WP7F5iJyagYqIZtz+4uf7gBvUDdmvXz3sGr1VAoGAdXTD6eeKeiI6PlhKBztF
|
||||
zOb3EQOO0SsLv3fnodu7ZaHbUgLaoTMPuB17r2jgrYM7FKQCBxTNdfGZmmfDjlLB
|
||||
0xZ5wL8ibU30ZXL8zTlWPElST9sto4B+FYVVF/vcG9sWeUUb2ncPcJ/Po3UAktDG
|
||||
jYQTTyuNGtSJHpad/YOZctkCgYBtWRaC7bq3of0rJGFOhdQT9SwItN/lrfj8hyHA
|
||||
OjpqTV4NfPmhsAtu6j96OZaeQc+FHvgXwt06cE6Rt4RG4uNPRluTFgO7XYFDfitP
|
||||
vCppnoIw6S5BBvHwPP+uIhUX2bsi/dm8vu8tb+gSvo4PkwtFhEr6I9HglBKmcmog
|
||||
q6waEQKBgHyecFBeM6Ls11Cd64vborwJPAuxIW7HBAFj/BS99oeG4TjBx4Sz2dFd
|
||||
rzUibJt4ndnHIvCN8JQkjNG14i9hJln+H3mRss8fbZ9vQdqG+2vOWADYSzzsNI55
|
||||
RFY7JjluKcVkp/zCDeUxTU3O6sS+v6/3VE11Cob6OYQx3lN5wrZ3
|
||||
-----END RSA PRIVATE KEY-----
|
|
@ -1,152 +0,0 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_auth_ldap_SUITE).
|
||||
|
||||
-compile(export_all).
|
||||
-compile(nowarn_export_all).
|
||||
|
||||
-include_lib("emqx/include/emqx.hrl").
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
-include_lib("common_test/include/ct.hrl").
|
||||
|
||||
-define(PID, emqx_auth_ldap).
|
||||
|
||||
-define(APP, emqx_auth_ldap).
|
||||
|
||||
-define(DeviceDN, "ou=test_device,dc=emqx,dc=io").
|
||||
|
||||
-define(AuthDN, "ou=test_auth,dc=emqx,dc=io").
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Setups
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
all() ->
|
||||
[{group, nossl}, {group, ssl}].
|
||||
|
||||
groups() ->
|
||||
Cases = emqx_ct:all(?MODULE),
|
||||
[{nossl, Cases}, {ssl, Cases}].
|
||||
|
||||
init_per_group(GrpName, Cfg) ->
|
||||
Fun = fun(App) -> set_special_configs(GrpName, App) end,
|
||||
emqx_ct_helpers:start_apps([emqx_auth_ldap], Fun),
|
||||
Cfg.
|
||||
|
||||
end_per_group(_GrpName, _Cfg) ->
|
||||
emqx_ct_helpers:stop_apps([emqx_auth_ldap]).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Cases
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
t_check_auth(_) ->
|
||||
MqttUser1 = #{clientid => <<"mqttuser1">>,
|
||||
username => <<"mqttuser0001">>,
|
||||
password => <<"mqttuser0001">>,
|
||||
zone => external},
|
||||
MqttUser2 = #{clientid => <<"mqttuser2">>,
|
||||
username => <<"mqttuser0002">>,
|
||||
password => <<"mqttuser0002">>,
|
||||
zone => external},
|
||||
MqttUser3 = #{clientid => <<"mqttuser3">>,
|
||||
username => <<"mqttuser0003">>,
|
||||
password => <<"mqttuser0003">>,
|
||||
zone => external},
|
||||
MqttUser4 = #{clientid => <<"mqttuser4">>,
|
||||
username => <<"mqttuser0004">>,
|
||||
password => <<"mqttuser0004">>,
|
||||
zone => external},
|
||||
MqttUser5 = #{clientid => <<"mqttuser5">>,
|
||||
username => <<"mqttuser0005">>,
|
||||
password => <<"mqttuser0005">>,
|
||||
zone => external},
|
||||
NonExistUser1 = #{clientid => <<"mqttuser6">>,
|
||||
username => <<"mqttuser0006">>,
|
||||
password => <<"mqttuser0006">>,
|
||||
zone => external},
|
||||
NonExistUser2 = #{clientid => <<"mqttuser7">>,
|
||||
username => <<"mqttuser0005">>,
|
||||
password => <<"mqttuser0006">>,
|
||||
zone => external},
|
||||
ct:log("MqttUser: ~p", [emqx_access_control:authenticate(MqttUser1)]),
|
||||
?assertMatch({ok, #{auth_result := success}}, emqx_access_control:authenticate(MqttUser1)),
|
||||
?assertMatch({ok, #{auth_result := success}}, emqx_access_control:authenticate(MqttUser2)),
|
||||
?assertMatch({ok, #{auth_result := success}}, emqx_access_control:authenticate(MqttUser3)),
|
||||
?assertMatch({ok, #{auth_result := success}}, emqx_access_control:authenticate(MqttUser4)),
|
||||
?assertMatch({ok, #{auth_result := success}}, emqx_access_control:authenticate(MqttUser5)),
|
||||
?assertEqual({error, not_authorized}, emqx_access_control:authenticate(NonExistUser1)),
|
||||
?assertEqual({error, bad_username_or_password}, emqx_access_control:authenticate(NonExistUser2)).
|
||||
|
||||
t_check_acl(_) ->
|
||||
MqttUser = #{clientid => <<"mqttuser1">>, username => <<"mqttuser0001">>, zone => external},
|
||||
NoMqttUser = #{clientid => <<"mqttuser2">>, username => <<"mqttuser0007">>, zone => external},
|
||||
allow = emqx_access_control:check_acl(MqttUser, publish, <<"mqttuser0001/pub/1">>),
|
||||
allow = emqx_access_control:check_acl(MqttUser, publish, <<"mqttuser0001/pub/+">>),
|
||||
allow = emqx_access_control:check_acl(MqttUser, publish, <<"mqttuser0001/pub/#">>),
|
||||
|
||||
allow = emqx_access_control:check_acl(MqttUser, subscribe, <<"mqttuser0001/sub/1">>),
|
||||
allow = emqx_access_control:check_acl(MqttUser, subscribe, <<"mqttuser0001/sub/+">>),
|
||||
allow = emqx_access_control:check_acl(MqttUser, subscribe, <<"mqttuser0001/sub/#">>),
|
||||
|
||||
allow = emqx_access_control:check_acl(MqttUser, publish, <<"mqttuser0001/pubsub/1">>),
|
||||
allow = emqx_access_control:check_acl(MqttUser, publish, <<"mqttuser0001/pubsub/+">>),
|
||||
allow = emqx_access_control:check_acl(MqttUser, publish, <<"mqttuser0001/pubsub/#">>),
|
||||
allow = emqx_access_control:check_acl(MqttUser, subscribe, <<"mqttuser0001/pubsub/1">>),
|
||||
allow = emqx_access_control:check_acl(MqttUser, subscribe, <<"mqttuser0001/pubsub/+">>),
|
||||
allow = emqx_access_control:check_acl(MqttUser, subscribe, <<"mqttuser0001/pubsub/#">>),
|
||||
|
||||
deny = emqx_access_control:check_acl(NoMqttUser, publish, <<"mqttuser0001/req/mqttuser0001/+">>),
|
||||
deny = emqx_access_control:check_acl(MqttUser, publish, <<"mqttuser0001/req/mqttuser0002/+">>),
|
||||
deny = emqx_access_control:check_acl(MqttUser, subscribe, <<"mqttuser0001/req/+/mqttuser0002">>),
|
||||
ok.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Helpers
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
set_special_configs(_, emqx) ->
|
||||
application:set_env(emqx, allow_anonymous, false),
|
||||
application:set_env(emqx, enable_acl_cache, false),
|
||||
application:set_env(emqx, acl_nomatch, deny),
|
||||
AclFilePath = filename:join(["test", "emqx_SUITE_data", "acl.conf"]),
|
||||
application:set_env(emqx, acl_file,
|
||||
emqx_ct_helpers:deps_path(emqx, AclFilePath)),
|
||||
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(Ssl, emqx_auth_ldap) ->
|
||||
case Ssl == ssl of
|
||||
true ->
|
||||
LdapOpts = application:get_env(emqx_auth_ldap, ldap, []),
|
||||
Path = emqx_ct_helpers:deps_path(emqx_auth_ldap, "test/certs/"),
|
||||
SslOpts = [{verify, verify_peer},
|
||||
{fail_if_no_peer_cert, true},
|
||||
{server_name_indication, disable},
|
||||
{keyfile, Path ++ "/client-key.pem"},
|
||||
{certfile, Path ++ "/client-cert.pem"},
|
||||
{cacertfile, Path ++ "/cacert.pem"}],
|
||||
LdapOpts1 = lists:keystore(ssl, 1, LdapOpts, {ssl, true}),
|
||||
LdapOpts2 = lists:keystore(sslopts, 1, LdapOpts1, {sslopts, SslOpts}),
|
||||
LdapOpts3 = lists:keystore(port, 1, LdapOpts2, {port, 636}),
|
||||
application:set_env(emqx_auth_ldap, ldap, LdapOpts3);
|
||||
_ ->
|
||||
ok
|
||||
end,
|
||||
application:set_env(emqx_auth_ldap, device_dn, "ou=testdevice, dc=emqx, dc=io").
|
||||
|
|
@ -1,112 +0,0 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_auth_ldap_bind_as_user_SUITE).
|
||||
|
||||
-compile(nowarn_export_all).
|
||||
-compile(export_all).
|
||||
|
||||
-include_lib("emqx/include/emqx.hrl").
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
-include_lib("common_test/include/ct.hrl").
|
||||
|
||||
-define(PID, emqx_auth_ldap).
|
||||
|
||||
-define(APP, emqx_auth_ldap).
|
||||
|
||||
-define(DeviceDN, "ou=test_device,dc=emqx,dc=io").
|
||||
|
||||
-define(AuthDN, "ou=test_auth,dc=emqx,dc=io").
|
||||
|
||||
all() ->
|
||||
[check_auth,
|
||||
check_acl].
|
||||
|
||||
init_per_suite(Config) ->
|
||||
emqx_ct_helpers:start_apps([emqx_auth_ldap], fun set_special_configs/1),
|
||||
Config.
|
||||
|
||||
end_per_suite(_Config) ->
|
||||
emqx_ct_helpers:stop_apps([emqx_auth_ldap]).
|
||||
|
||||
check_auth(_) ->
|
||||
MqttUser1 = #{clientid => <<"mqttuser1">>,
|
||||
username => <<"user1">>,
|
||||
password => <<"mqttuser0001">>,
|
||||
zone => external},
|
||||
MqttUser2 = #{clientid => <<"mqttuser2">>,
|
||||
username => <<"user2">>,
|
||||
password => <<"mqttuser0002">>,
|
||||
zone => external},
|
||||
NonExistUser1 = #{clientid => <<"mqttuser3">>,
|
||||
username => <<"user3">>,
|
||||
password => <<"mqttuser0003">>,
|
||||
zone => external},
|
||||
ct:log("MqttUser: ~p", [emqx_access_control:authenticate(MqttUser1)]),
|
||||
?assertMatch({ok, #{auth_result := success}}, emqx_access_control:authenticate(MqttUser1)),
|
||||
?assertMatch({ok, #{auth_result := success}}, emqx_access_control:authenticate(MqttUser2)),
|
||||
?assertEqual({error, not_authorized}, emqx_access_control:authenticate(NonExistUser1)).
|
||||
|
||||
check_acl(_) ->
|
||||
MqttUser = #{clientid => <<"mqttuser1">>, username => <<"user1">>, zone => external},
|
||||
NoMqttUser = #{clientid => <<"mqttuser2">>, username => <<"user7">>, zone => external},
|
||||
allow = emqx_access_control:check_acl(MqttUser, publish, <<"mqttuser0001/pub/1">>),
|
||||
allow = emqx_access_control:check_acl(MqttUser, publish, <<"mqttuser0001/pub/+">>),
|
||||
allow = emqx_access_control:check_acl(MqttUser, publish, <<"mqttuser0001/pub/#">>),
|
||||
|
||||
allow = emqx_access_control:check_acl(MqttUser, subscribe, <<"mqttuser0001/sub/1">>),
|
||||
allow = emqx_access_control:check_acl(MqttUser, subscribe, <<"mqttuser0001/sub/+">>),
|
||||
allow = emqx_access_control:check_acl(MqttUser, subscribe, <<"mqttuser0001/sub/#">>),
|
||||
|
||||
allow = emqx_access_control:check_acl(MqttUser, publish, <<"mqttuser0001/pubsub/1">>),
|
||||
allow = emqx_access_control:check_acl(MqttUser, publish, <<"mqttuser0001/pubsub/+">>),
|
||||
allow = emqx_access_control:check_acl(MqttUser, publish, <<"mqttuser0001/pubsub/#">>),
|
||||
allow = emqx_access_control:check_acl(MqttUser, subscribe, <<"mqttuser0001/pubsub/1">>),
|
||||
allow = emqx_access_control:check_acl(MqttUser, subscribe, <<"mqttuser0001/pubsub/+">>),
|
||||
allow = emqx_access_control:check_acl(MqttUser, subscribe, <<"mqttuser0001/pubsub/#">>),
|
||||
|
||||
deny = emqx_access_control:check_acl(NoMqttUser, publish, <<"mqttuser0001/req/mqttuser0001/+">>),
|
||||
deny = emqx_access_control:check_acl(MqttUser, publish, <<"mqttuser0001/req/mqttuser0002/+">>),
|
||||
deny = emqx_access_control:check_acl(MqttUser, subscribe, <<"mqttuser0001/req/+/mqttuser0002">>),
|
||||
ok.
|
||||
|
||||
set_special_configs(emqx) ->
|
||||
application:set_env(emqx, allow_anonymous, false),
|
||||
application:set_env(emqx, enable_acl_cache, false),
|
||||
application:set_env(emqx, acl_nomatch, deny),
|
||||
AclFilePath = filename:join(["test", "emqx_SUITE_data", "acl.conf"]),
|
||||
application:set_env(emqx, acl_file,
|
||||
emqx_ct_helpers:deps_path(emqx, AclFilePath)),
|
||||
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(emqx_auth_ldap) ->
|
||||
application:set_env(emqx_auth_ldap, bind_as_user, true),
|
||||
application:set_env(emqx_auth_ldap, device_dn, "ou=testdevice, dc=emqx, dc=io"),
|
||||
application:set_env(emqx_auth_ldap, custom_base_dn, "${device_dn}"),
|
||||
%% auth.ldap.filters.1.key = mqttAccountName
|
||||
%% auth.ldap.filters.1.value = ${user}
|
||||
%% auth.ldap.filters.1.op = and
|
||||
%% auth.ldap.filters.2.key = objectClass
|
||||
%% auth.ldap.filters.1.value = mqttUser
|
||||
application:set_env(emqx_auth_ldap, filters, [{"mqttAccountName", "${user}"},
|
||||
"and",
|
||||
{"objectClass", "mqttUser"}]);
|
||||
|
||||
set_special_configs(_App) ->
|
||||
ok.
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
.eunit
|
||||
deps
|
||||
*.o
|
||||
*.beam
|
||||
*.plt
|
||||
erl_crash.dump
|
||||
ebin
|
||||
rel/example_project
|
||||
.concrete/DEV_MODE
|
||||
.rebar
|
||||
.erlang.mk/
|
||||
emqx_auth_mnesia.d
|
||||
data/
|
||||
_build/
|
||||
.DS_Store
|
||||
cover/
|
||||
ct.coverdata
|
||||
eunit.coverdata
|
||||
logs/
|
||||
test/ct.cover.spec
|
||||
rebar.lock
|
||||
rebar3.crashdump
|
||||
erlang.mk
|
||||
.*.swp
|
||||
.rebar3/
|
||||
etc/emqx_auth_mnesia.conf.rendered
|
|
@ -1,2 +0,0 @@
|
|||
emqx_auth_mnesia
|
||||
===============
|
|
@ -1,30 +0,0 @@
|
|||
## 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,38 +0,0 @@
|
|||
-define(APP, emqx_auth_mnesia).
|
||||
|
||||
-type(login():: {clientid, binary()}
|
||||
| {username, binary()}).
|
||||
|
||||
-record(emqx_user, {
|
||||
login :: login(),
|
||||
password :: binary(),
|
||||
created_at :: integer()
|
||||
}).
|
||||
|
||||
-record(emqx_acl, {
|
||||
filter:: {login() | all, emqx_topic:topic()},
|
||||
action :: pub | sub | pubsub,
|
||||
access :: allow | deny,
|
||||
created_at :: integer()
|
||||
}).
|
||||
|
||||
-record(auth_metrics, {
|
||||
success = 'client.auth.success',
|
||||
failure = 'client.auth.failure',
|
||||
ignore = 'client.auth.ignore'
|
||||
}).
|
||||
|
||||
-record(acl_metrics, {
|
||||
allow = 'client.acl.allow',
|
||||
deny = 'client.acl.deny',
|
||||
ignore = 'client.acl.ignore'
|
||||
}).
|
||||
|
||||
-define(METRICS(Type), tl(tuple_to_list(#Type{}))).
|
||||
-define(METRICS(Type, K), #Type{}#Type.K).
|
||||
|
||||
-define(AUTH_METRICS, ?METRICS(auth_metrics)).
|
||||
-define(AUTH_METRICS(K), ?METRICS(auth_metrics, K)).
|
||||
|
||||
-define(ACL_METRICS, ?METRICS(acl_metrics)).
|
||||
-define(ACL_METRICS(K), ?METRICS(acl_metrics, K)).
|
|
@ -1,43 +0,0 @@
|
|||
%%-*- mode: erlang -*-
|
||||
%% emqx_auth_mnesia config mapping
|
||||
|
||||
{mapping, "auth.mnesia.password_hash", "emqx_auth_mnesia.password_hash", [
|
||||
{default, sha256},
|
||||
{datatype, {enum, [plain, md5, sha, sha256, sha512]}}
|
||||
]}.
|
||||
|
||||
{mapping, "auth.client.$id.clientid", "emqx_auth_mnesia.clientid_list", [
|
||||
{datatype, string}
|
||||
]}.
|
||||
|
||||
{mapping, "auth.client.$id.password", "emqx_auth_mnesia.clientid_list", [
|
||||
{datatype, string}
|
||||
]}.
|
||||
|
||||
{translation, "emqx_auth_mnesia.clientid_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}.
|
||||
|
||||
{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
|
||||
end, [], Userlist)
|
||||
end}.
|
|
@ -1,17 +0,0 @@
|
|||
{deps,
|
||||
[ ]}.
|
||||
|
||||
{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}.
|
||||
|
|
@ -1,104 +0,0 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_acl_mnesia).
|
||||
|
||||
-include("emqx_auth_mnesia.hrl").
|
||||
|
||||
-include_lib("stdlib/include/ms_transform.hrl").
|
||||
|
||||
-define(TABLE, emqx_acl).
|
||||
|
||||
%% ACL Callbacks
|
||||
-export([ init/0
|
||||
, register_metrics/0
|
||||
, check_acl/5
|
||||
, description/0
|
||||
]).
|
||||
|
||||
init() ->
|
||||
ok = ekka_mnesia:create_table(emqx_acl, [
|
||||
{type, bag},
|
||||
{disc_copies, [node()]},
|
||||
{attributes, record_info(fields, emqx_acl)},
|
||||
{storage_properties, [{ets, [{read_concurrency, true}]}]}]),
|
||||
ok = ekka_mnesia:copy_table(emqx_acl, disc_copies).
|
||||
|
||||
-spec(register_metrics() -> ok).
|
||||
register_metrics() ->
|
||||
lists:foreach(fun emqx_metrics:ensure/1, ?ACL_METRICS).
|
||||
|
||||
check_acl(ClientInfo = #{ clientid := Clientid }, PubSub, Topic, _NoMatchAction, _Params) ->
|
||||
Username = maps:get(username, ClientInfo, undefined),
|
||||
|
||||
Acls = case Username of
|
||||
undefined ->
|
||||
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.
|
||||
|
||||
description() -> "Acl with Mnesia".
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Internal functions
|
||||
%%-------------------------------------------------------------------
|
||||
|
||||
match(_ClientInfo, _PubSub, _Topic, []) ->
|
||||
nomatch;
|
||||
match(ClientInfo, PubSub, Topic, [ {_, ACLTopic, Action, Access, _} | Acls]) ->
|
||||
case match_actions(PubSub, Action) andalso match_topic(ClientInfo, Topic, ACLTopic) of
|
||||
true -> Access;
|
||||
false -> match(ClientInfo, PubSub, Topic, Acls)
|
||||
end.
|
||||
|
||||
match_topic(ClientInfo, Topic, ACLTopic) when is_binary(Topic) ->
|
||||
emqx_topic:match(Topic, feed_var(ClientInfo, ACLTopic)).
|
||||
|
||||
match_actions(_, pubsub) -> true;
|
||||
match_actions(subscribe, sub) -> true;
|
||||
match_actions(publish, pub) -> true;
|
||||
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,226 +0,0 @@
|
|||
%c%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_acl_mnesia_api).
|
||||
|
||||
-include("emqx_auth_mnesia.hrl").
|
||||
|
||||
-include_lib("stdlib/include/ms_transform.hrl").
|
||||
|
||||
-import(proplists, [ get_value/2
|
||||
, get_value/3
|
||||
]).
|
||||
|
||||
-import(minirest, [return/1]).
|
||||
|
||||
-rest_api(#{name => list_clientid,
|
||||
method => 'GET',
|
||||
path => "/acl/clientid",
|
||||
func => list_clientid,
|
||||
descr => "List available mnesia in the cluster"
|
||||
}).
|
||||
|
||||
-rest_api(#{name => list_username,
|
||||
method => 'GET',
|
||||
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,
|
||||
descr => "Lookup mnesia in the cluster"
|
||||
}).
|
||||
|
||||
-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',
|
||||
path => "/acl",
|
||||
func => add,
|
||||
descr => "Add mnesia in the cluster"
|
||||
}).
|
||||
|
||||
-rest_api(#{name => delete_clientid,
|
||||
method => 'DELETE',
|
||||
path => "/acl/clientid/:bin:clientid/topic/:bin:topic",
|
||||
func => delete,
|
||||
descr => "Delete mnesia in the cluster"
|
||||
}).
|
||||
|
||||
-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
|
||||
, add/2
|
||||
, delete/2
|
||||
]).
|
||||
|
||||
list_clientid(_Bindings, Params) ->
|
||||
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)}).
|
||||
|
||||
list_username(_Bindings, Params) ->
|
||||
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) ->
|
||||
[ P | _] = Params,
|
||||
case is_list(P) of
|
||||
true -> return(do_add(Params, []));
|
||||
false ->
|
||||
Re = do_add(Params),
|
||||
case Re of
|
||||
#{result := ok} -> return({ok, Re});
|
||||
#{result := <<"ok">>} -> return({ok, Re});
|
||||
_ -> return({error, {add, Re}})
|
||||
end
|
||||
end.
|
||||
|
||||
do_add([ Params | ParamsN ], ReList) ->
|
||||
do_add(ParamsN, [do_add(Params) | ReList]);
|
||||
|
||||
do_add([], ReList) ->
|
||||
{ok, ReList}.
|
||||
|
||||
do_add(Params) ->
|
||||
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
|
||||
%%------------------------------------------------------------------------------
|
||||
format({{clientid, Clientid}, Topic, Action, Access, _CreatedAt}) ->
|
||||
#{clientid => Clientid, topic => Topic, action => Action, access => Access};
|
||||
format({{username, Username}, Topic, Action, Access, _CreatedAt}) ->
|
||||
#{username => Username, topic => Topic, action => Action, access => Access};
|
||||
format({all, Topic, Action, Access, _CreatedAt}) ->
|
||||
#{all => '$all', topic => Topic, action => Action, access => Access};
|
||||
format(List) when is_list(List) ->
|
||||
format(List, []).
|
||||
|
||||
format([L | List], Relist) ->
|
||||
format(List, [format(L) | Relist]);
|
||||
format([], ReList) -> lists:reverse(ReList).
|
||||
|
||||
validate([], []) ->
|
||||
ok;
|
||||
validate([K|Keys], [V|Values]) ->
|
||||
case do_validation(K, V) of
|
||||
false -> {error, K};
|
||||
true -> validate(Keys, Values)
|
||||
end.
|
||||
do_validation(login, all) ->
|
||||
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(topic, V) when is_binary(V)
|
||||
andalso byte_size(V) > 0 ->
|
||||
true;
|
||||
do_validation(action, V) when is_binary(V) ->
|
||||
case V =:= <<"pub">> orelse V =:= <<"sub">> orelse V =:= <<"pubsub">> of
|
||||
true -> true;
|
||||
false -> false
|
||||
end;
|
||||
do_validation(access, V) when V =:= <<"allow">> orelse V =:= <<"deny">> ->
|
||||
true;
|
||||
do_validation(_, _) ->
|
||||
false.
|
||||
|
||||
format_msg(Message)
|
||||
when is_atom(Message);
|
||||
is_binary(Message) -> Message;
|
||||
|
||||
format_msg(Message) when is_tuple(Message) ->
|
||||
iolist_to_binary(io_lib:format("~p", [Message])).
|
||||
|
||||
urldecode(S) ->
|
||||
emqx_http_lib:uri_decode(S).
|
|
@ -1,270 +0,0 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_acl_mnesia_cli).
|
||||
|
||||
-include("emqx_auth_mnesia.hrl").
|
||||
-include_lib("emqx/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) ->
|
||||
Filter = {Login, Topic},
|
||||
Acl = #?TABLE{
|
||||
filter = Filter,
|
||||
action = Action,
|
||||
access = Access,
|
||||
created_at = erlang:system_time(millisecond)
|
||||
},
|
||||
ret(mnesia:transaction(
|
||||
fun() ->
|
||||
OldRecords = mnesia:wread({?TABLE, Filter}),
|
||||
case Action of
|
||||
pubsub ->
|
||||
update_permission(pub, Acl, OldRecords),
|
||||
update_permission(sub, Acl, OldRecords);
|
||||
_ ->
|
||||
update_permission(Action, Acl, OldRecords)
|
||||
end
|
||||
end)).
|
||||
|
||||
%% @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)).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% ACL Cli
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
cli(["list"]) ->
|
||||
[print_acl(Acl) || Acl <- all_acls()];
|
||||
|
||||
cli(["list", "clientid"]) ->
|
||||
[print_acl(Acl) || Acl <- all_acls(clientid)];
|
||||
|
||||
cli(["list", "username"]) ->
|
||||
[print_acl(Acl) || Acl <- all_acls(username)];
|
||||
|
||||
cli(["list", "_all"]) ->
|
||||
[print_acl(Acl) || Acl <- 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]) ->
|
||||
[print_acl(Acl) || Acl <- lookup_acl({clientid, iolist_to_binary(Clientid)})];
|
||||
|
||||
cli(["show", "username", Username]) ->
|
||||
[print_acl(Acl) || Acl <- lookup_acl({username, iolist_to_binary(Username)})];
|
||||
|
||||
cli(["del", "clientid", Clientid, Topic])->
|
||||
cli(["delete", "clientid", Clientid, Topic]);
|
||||
|
||||
cli(["delete", "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])->
|
||||
cli(["delete", "username", Username, Topic]);
|
||||
|
||||
cli(["delete", "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])->
|
||||
cli(["delete", "_all", Topic]);
|
||||
|
||||
cli(["delete", "_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 delete clientid <Clientid> <Topic>", "Delete clientid acl"}
|
||||
, {"acl delete username <Username> <Topic>", "Delete username acl"}
|
||||
, {"acl delete _all <Topic>", "Delete $all acl"}
|
||||
]).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% 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.
|
||||
|
||||
print_acl({{clientid, Clientid}, Topic, Action, Access, _}) ->
|
||||
emqx_ctl:print(
|
||||
"Acl(clientid = ~p topic = ~p action = ~p access = ~p)~n",
|
||||
[Clientid, Topic, Action, Access]
|
||||
);
|
||||
print_acl({{username, Username}, Topic, Action, Access, _}) ->
|
||||
emqx_ctl:print(
|
||||
"Acl(username = ~p topic = ~p action = ~p access = ~p)~n",
|
||||
[Username, Topic, Action, Access]
|
||||
);
|
||||
print_acl({all, Topic, Action, Access, _}) ->
|
||||
emqx_ctl:print(
|
||||
"Acl($all topic = ~p action = ~p access = ~p)~n",
|
||||
[Topic, Action, Access]
|
||||
).
|
||||
|
||||
update_permission(Action, Acl0, OldRecords) ->
|
||||
Acl = Acl0 #?TABLE{action = Action},
|
||||
maybe_delete_shadowed_records(Action, OldRecords),
|
||||
mnesia:write(Acl).
|
||||
|
||||
maybe_delete_shadowed_records(_, []) ->
|
||||
ok;
|
||||
maybe_delete_shadowed_records(Action1, [Rec = #emqx_acl{action = Action2} | Rest]) ->
|
||||
if Action1 =:= Action2 ->
|
||||
ok = mnesia:delete_object(Rec);
|
||||
Action2 =:= pubsub ->
|
||||
%% Perform migration from the old data format on the
|
||||
%% fly. This is needed only for the enterprise version,
|
||||
%% delete this branch on 5.0
|
||||
mnesia:delete_object(Rec),
|
||||
mnesia:write(Rec#?TABLE{action = other_action(Action1)});
|
||||
true ->
|
||||
ok
|
||||
end,
|
||||
maybe_delete_shadowed_records(Action1, Rest).
|
||||
|
||||
other_action(pub) -> sub;
|
||||
other_action(sub) -> pub.
|
|
@ -1,14 +0,0 @@
|
|||
{application, emqx_auth_mnesia,
|
||||
[{description, "EMQ X Authentication with Mnesia"},
|
||||
{vsn, "4.3.1"}, % strict semver, bump manually
|
||||
{modules, []},
|
||||
{registered, []},
|
||||
{applications, [kernel,stdlib,mnesia]},
|
||||
{mod, {emqx_auth_mnesia_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-mnesia"}
|
||||
]}
|
||||
]}.
|
|
@ -1,13 +0,0 @@
|
|||
%% -*-: erlang -*-
|
||||
{VSN,
|
||||
[
|
||||
{"4.3.0", [
|
||||
{restart_application, emqx_auth_mnesia}
|
||||
]}
|
||||
],
|
||||
[
|
||||
{"4.3.0", [
|
||||
{restart_application, emqx_auth_mnesia}
|
||||
]}
|
||||
]
|
||||
}.
|
|
@ -1,109 +0,0 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_auth_mnesia).
|
||||
|
||||
-include("emqx_auth_mnesia.hrl").
|
||||
|
||||
-include_lib("emqx/include/emqx.hrl").
|
||||
-include_lib("emqx/include/logger.hrl").
|
||||
-include_lib("emqx/include/types.hrl").
|
||||
|
||||
-include_lib("stdlib/include/ms_transform.hrl").
|
||||
|
||||
-define(TABLE, emqx_user).
|
||||
%% Auth callbacks
|
||||
-export([ init/1
|
||||
, register_metrics/0
|
||||
, check/3
|
||||
, description/0
|
||||
]).
|
||||
|
||||
init(#{clientid_list := ClientidList, username_list := UsernameList}) ->
|
||||
ok = ekka_mnesia:create_table(?TABLE, [
|
||||
{disc_copies, [node()]},
|
||||
{attributes, record_info(fields, emqx_user)},
|
||||
{storage_properties, [{ets, [{read_concurrency, true}]}]}]),
|
||||
_ = [ 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(?TABLE, disc_copies).
|
||||
|
||||
%% @private
|
||||
add_default_user({Login, Password}) when is_tuple(Login) ->
|
||||
emqx_auth_mnesia_cli:add_user(Login, Password).
|
||||
|
||||
-spec(register_metrics() -> ok).
|
||||
register_metrics() ->
|
||||
lists:foreach(fun emqx_metrics:ensure/1, ?AUTH_METRICS).
|
||||
|
||||
check(ClientInfo = #{ clientid := Clientid
|
||||
, password := NPassword
|
||||
}, 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)),
|
||||
ok;
|
||||
List ->
|
||||
case match_password(NPassword, HashType, List) of
|
||||
false ->
|
||||
?LOG(error, "[Mnesia] Auth from mnesia failed: ~p", [ClientInfo]),
|
||||
emqx_metrics:inc(?AUTH_METRICS(failure)),
|
||||
{stop, AuthResult#{anonymous => false, auth_result => password_error}};
|
||||
_ ->
|
||||
emqx_metrics:inc(?AUTH_METRICS(success)),
|
||||
{stop, AuthResult#{anonymous => false, auth_result => success}}
|
||||
end
|
||||
end.
|
||||
|
||||
description() -> "Authentication with Mnesia".
|
||||
|
||||
match_password(Password, HashType, HashList) ->
|
||||
lists:any(
|
||||
fun(Secret) ->
|
||||
case is_salt_hash(Secret, HashType) of
|
||||
true ->
|
||||
<<Salt:4/binary, Hash/binary>> = Secret,
|
||||
Hash =:= hash(Password, Salt, HashType);
|
||||
_ ->
|
||||
Secret =:= hash(Password, HashType)
|
||||
end
|
||||
end, HashList).
|
||||
|
||||
hash(undefined, HashType) ->
|
||||
hash(<<>>, HashType);
|
||||
hash(Password, HashType) ->
|
||||
emqx_passwd:hash(HashType, Password).
|
||||
|
||||
hash(undefined, SaltBin, HashType) ->
|
||||
hash(<<>>, SaltBin, HashType);
|
||||
hash(Password, SaltBin, HashType) ->
|
||||
emqx_passwd:hash(HashType, <<SaltBin/binary, Password/binary>>).
|
||||
|
||||
is_salt_hash(_, plain) ->
|
||||
true;
|
||||
is_salt_hash(Secret, HashType) ->
|
||||
not (byte_size(Secret) == len(HashType)).
|
||||
|
||||
len(md5) -> 32;
|
||||
len(sha) -> 40;
|
||||
len(sha256) -> 64;
|
||||
len(sha512) -> 128.
|
|
@ -1,305 +0,0 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_auth_mnesia_api).
|
||||
|
||||
-include_lib("stdlib/include/qlc.hrl").
|
||||
-include_lib("stdlib/include/ms_transform.hrl").
|
||||
|
||||
-define(TABLE, emqx_user).
|
||||
|
||||
-import(proplists, [get_value/2]).
|
||||
-import(minirest, [return/1]).
|
||||
-export([paginate/5]).
|
||||
|
||||
-export([ list_clientid/2
|
||||
, lookup_clientid/2
|
||||
, add_clientid/2
|
||||
, update_clientid/2
|
||||
, delete_clientid/2
|
||||
]).
|
||||
|
||||
-rest_api(#{name => list_clientid,
|
||||
method => 'GET',
|
||||
path => "/auth_clientid",
|
||||
func => list_clientid,
|
||||
descr => "List available clientid in the cluster"
|
||||
}).
|
||||
|
||||
-rest_api(#{name => lookup_clientid,
|
||||
method => 'GET',
|
||||
path => "/auth_clientid/:bin:clientid",
|
||||
func => lookup_clientid,
|
||||
descr => "Lookup clientid in the cluster"
|
||||
}).
|
||||
|
||||
-rest_api(#{name => add_clientid,
|
||||
method => 'POST',
|
||||
path => "/auth_clientid",
|
||||
func => add_clientid,
|
||||
descr => "Add clientid in the cluster"
|
||||
}).
|
||||
|
||||
-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,
|
||||
case is_list(P) of
|
||||
true -> return(do_add_clientid(Params, []));
|
||||
false ->
|
||||
Re = do_add_clientid(Params),
|
||||
case Re of
|
||||
ok -> return(ok);
|
||||
{error, Error} -> return({error, format_msg(Error)})
|
||||
end
|
||||
end.
|
||||
|
||||
do_add_clientid([ Params | ParamsN ], ReList ) ->
|
||||
Clientid = urldecode(get_value(<<"clientid">>, Params)),
|
||||
do_add_clientid(ParamsN, [{Clientid, format_msg(do_add_clientid(Params))} | ReList]);
|
||||
|
||||
do_add_clientid([], ReList) ->
|
||||
{ok, ReList}.
|
||||
|
||||
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),
|
||||
case validate([password], [Password]) of
|
||||
ok -> return(emqx_auth_mnesia_cli:update_user({clientid, urldecode(Clientid)}, urldecode(Password)));
|
||||
Err -> return(Err)
|
||||
end.
|
||||
|
||||
delete_clientid(#{clientid := Clientid}, _) ->
|
||||
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);
|
||||
{error, 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
|
||||
%%------------------------------------------------------------------------------
|
||||
|
||||
paginate(Tables, MatchSpec, Params, ComparingFun, RowFun) ->
|
||||
Qh = query_handle(Tables, MatchSpec),
|
||||
Count = count(Tables, MatchSpec),
|
||||
Page = page(Params),
|
||||
Limit = limit(Params),
|
||||
Cursor = qlc:cursor(Qh),
|
||||
case Page > 1 of
|
||||
true ->
|
||||
_ = qlc:next_answers(Cursor, (Page - 1) * Limit),
|
||||
ok;
|
||||
false -> ok
|
||||
end,
|
||||
Rows = qlc:next_answers(Cursor, Limit),
|
||||
qlc:delete_cursor(Cursor),
|
||||
#{meta => #{page => Page, limit => Limit, count => Count},
|
||||
data => [RowFun(Row) || Row <- lists:sort(ComparingFun, Rows)]}.
|
||||
|
||||
query_handle(Table, MatchSpec) when is_atom(Table) ->
|
||||
Options = {traverse, {select, MatchSpec}},
|
||||
qlc:q([R|| R <- ets:table(Table, Options)]);
|
||||
query_handle([Table], MatchSpec) when is_atom(Table) ->
|
||||
Options = {traverse, {select, MatchSpec}},
|
||||
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, MatchSpec) when is_atom(Table) ->
|
||||
[{MatchPattern, Where, _Re}] = MatchSpec,
|
||||
NMatchSpec = [{MatchPattern, Where, [true]}],
|
||||
ets:select_count(Table, NMatchSpec);
|
||||
count([Table], MatchSpec) when is_atom(Table) ->
|
||||
[{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) ->
|
||||
binary_to_integer(proplists:get_value(<<"_page">>, Params, <<"1">>)).
|
||||
|
||||
limit(Params) ->
|
||||
case proplists:get_value(<<"_limit">>, Params) of
|
||||
undefined -> 10;
|
||||
Size -> binary_to_integer(Size)
|
||||
end.
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
%% Interval Funcs
|
||||
%%------------------------------------------------------------------------------
|
||||
|
||||
format([{?TABLE, {clientid, ClientId}, Password, _InterTime}]) ->
|
||||
#{clientid => ClientId,
|
||||
password => Password};
|
||||
|
||||
format([{?TABLE, {username, Username}, Password, _InterTime}]) ->
|
||||
#{username => Username,
|
||||
password => Password};
|
||||
|
||||
format([]) ->
|
||||
#{}.
|
||||
|
||||
validate([], []) ->
|
||||
ok;
|
||||
validate([K|Keys], [V|Values]) ->
|
||||
case do_validation(K, V) of
|
||||
false -> {error, K};
|
||||
true -> validate(Keys, Values)
|
||||
end.
|
||||
|
||||
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(password, V) when is_binary(V)
|
||||
andalso byte_size(V) > 0 ->
|
||||
true;
|
||||
do_validation(_, _) ->
|
||||
false.
|
||||
|
||||
format_msg(Message)
|
||||
when is_atom(Message);
|
||||
is_binary(Message) -> Message;
|
||||
|
||||
format_msg(Message) when is_tuple(Message) ->
|
||||
iolist_to_binary(io_lib:format("~p", [Message])).
|
||||
|
||||
urldecode(S) ->
|
||||
emqx_http_lib:uri_decode(S).
|
|
@ -1,68 +0,0 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_auth_mnesia_app).
|
||||
|
||||
-behaviour(application).
|
||||
|
||||
-emqx_plugin(auth).
|
||||
|
||||
-include("emqx_auth_mnesia.hrl").
|
||||
|
||||
%% Application callbacks
|
||||
-export([ start/2
|
||||
, prep_stop/1
|
||||
, stop/1
|
||||
]).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Application callbacks
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
start(_StartType, _StartArgs) ->
|
||||
{ok, Sup} = emqx_auth_mnesia_sup:start_link(),
|
||||
emqx_ctl:register_command(clientid, {emqx_auth_mnesia_cli, auth_clientid_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_acl_hook(),
|
||||
{ok, Sup}.
|
||||
|
||||
prep_stop(State) ->
|
||||
emqx:unhook('client.authenticate', {emqx_auth_mnesia, check}),
|
||||
emqx:unhook('client.check_acl', {emqx_acl_mnesia, check_acl}),
|
||||
emqx_ctl:unregister_command(clientid),
|
||||
emqx_ctl:unregister_command(user),
|
||||
emqx_ctl:unregister_command(acl),
|
||||
State.
|
||||
|
||||
stop(_State) ->
|
||||
ok.
|
||||
|
||||
load_auth_hook() ->
|
||||
ClientidList = application:get_env(?APP, clientid_list, []),
|
||||
UsernameList = application:get_env(?APP, username_list, []),
|
||||
ok = emqx_auth_mnesia:init(#{clientid_list => ClientidList, username_list => UsernameList}),
|
||||
ok = emqx_auth_mnesia:register_metrics(),
|
||||
Params = #{
|
||||
hash_type => application:get_env(emqx_auth_mnesia, password_hash, sha256)
|
||||
},
|
||||
emqx:hook('client.authenticate', {emqx_auth_mnesia, check, [Params]}).
|
||||
|
||||
load_acl_hook() ->
|
||||
ok = emqx_acl_mnesia:init(),
|
||||
ok = emqx_acl_mnesia:register_metrics(),
|
||||
emqx:hook('client.check_acl', {emqx_acl_mnesia, check_acl, [#{}]}).
|
|
@ -1,194 +0,0 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_auth_mnesia_cli).
|
||||
|
||||
-include("emqx_auth_mnesia.hrl").
|
||||
-include_lib("emqx/include/logger.hrl").
|
||||
-include_lib("stdlib/include/ms_transform.hrl").
|
||||
-define(TABLE, emqx_user).
|
||||
%% Auth APIs
|
||||
-export([ add_user/2
|
||||
, update_user/2
|
||||
, remove_user/1
|
||||
, lookup_user/1
|
||||
, all_users/0
|
||||
, all_users/1
|
||||
]).
|
||||
%% Cli
|
||||
-export([ auth_clientid_cli/1
|
||||
, auth_username_cli/1
|
||||
]).
|
||||
|
||||
%% Helper
|
||||
-export([comparing/2]).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Auth APIs
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
%% @doc Add User
|
||||
-spec(add_user(tuple(), binary()) -> ok | {error, any()}).
|
||||
add_user(Login, Password) ->
|
||||
User = #emqx_user{
|
||||
login = Login,
|
||||
password = encrypted_data(Password),
|
||||
created_at = erlang:system_time(millisecond)
|
||||
},
|
||||
ret(mnesia:transaction(fun insert_user/1, [User])).
|
||||
|
||||
insert_user(User = #emqx_user{login = Login}) ->
|
||||
case mnesia:read(?TABLE, Login) of
|
||||
[] -> mnesia:write(User);
|
||||
[_|_] -> mnesia:abort(existed)
|
||||
end.
|
||||
|
||||
%% @doc Update User
|
||||
-spec(update_user(tuple(), binary()) -> ok | {error, any()}).
|
||||
update_user(Login, NewPassword) ->
|
||||
ret(mnesia:transaction(fun do_update_user/2, [Login, encrypted_data(NewPassword)])).
|
||||
|
||||
do_update_user(Login, NewPassword) ->
|
||||
case mnesia:read(?TABLE, Login) of
|
||||
[#emqx_user{} = User] ->
|
||||
mnesia:write(User#emqx_user{password = NewPassword});
|
||||
[] -> mnesia:abort(noexisted)
|
||||
end.
|
||||
|
||||
%% @doc Lookup user by login
|
||||
-spec(lookup_user(tuple()) -> list()).
|
||||
lookup_user(undefined) -> [];
|
||||
lookup_user(Login) ->
|
||||
Re = mnesia:dirty_read(?TABLE, Login),
|
||||
lists:sort(fun comparing/2, Re).
|
||||
|
||||
%% @doc Remove user
|
||||
-spec(remove_user(tuple()) -> ok | {error, any()}).
|
||||
remove_user(Login) ->
|
||||
ret(mnesia:transaction(fun mnesia:delete/1, [{?TABLE, Login}])).
|
||||
|
||||
%% @doc All logins
|
||||
-spec(all_users() -> list()).
|
||||
all_users() -> mnesia:dirty_all_keys(?TABLE).
|
||||
|
||||
all_users(clientid) ->
|
||||
MatchSpec = ets:fun2ms(
|
||||
fun({?TABLE, {clientid, Clientid}, Password, CreatedAt}) ->
|
||||
{?TABLE, {clientid, Clientid}, Password, CreatedAt}
|
||||
end),
|
||||
lists:sort(fun comparing/2, ets:select(?TABLE, MatchSpec));
|
||||
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
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
comparing({?TABLE, _, _, CreatedAt1},
|
||||
{?TABLE, _, _, CreatedAt2}) ->
|
||||
CreatedAt1 >= CreatedAt2.
|
||||
|
||||
ret({atomic, ok}) -> ok;
|
||||
ret({aborted, Error}) -> {error, Error}.
|
||||
|
||||
encrypted_data(Password) ->
|
||||
HashType = application:get_env(emqx_auth_mnesia, 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() ->
|
||||
{_AlgHandler, _AlgState} = rand:seed(exsplus, erlang:timestamp()),
|
||||
Salt = rand:uniform(16#ffffffff), <<Salt:32>>.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Auth Clientid Cli
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
auth_clientid_cli(["list"]) ->
|
||||
[emqx_ctl:print("~s~n", [ClientId])
|
||||
|| {?TABLE, {clientid, ClientId}, _Password, _CreatedAt} <- all_users(clientid)
|
||||
];
|
||||
|
||||
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");
|
||||
{error, Reason} -> emqx_ctl:print("Error: ~p~n", [Reason])
|
||||
end;
|
||||
|
||||
auth_clientid_cli(["update", ClientId, NewPassword]) ->
|
||||
case update_user({clientid, iolist_to_binary(ClientId)}, iolist_to_binary(NewPassword)) of
|
||||
ok -> emqx_ctl:print("ok~n");
|
||||
{error, Reason} -> emqx_ctl:print("Error: ~p~n", [Reason])
|
||||
end;
|
||||
|
||||
auth_clientid_cli(["del", ClientId]) ->
|
||||
auth_clientid_cli(["delete", ClientId]);
|
||||
|
||||
auth_clientid_cli(["delete", ClientId]) ->
|
||||
case remove_user({clientid, iolist_to_binary(ClientId)}) of
|
||||
ok -> emqx_ctl:print("ok~n");
|
||||
{error, Reason} -> emqx_ctl:print("Error: ~p~n", [Reason])
|
||||
end;
|
||||
|
||||
auth_clientid_cli(_) ->
|
||||
emqx_ctl:usage([{"clientid list", "List clientid auth rules"},
|
||||
{"clientid add <Username> <Password>", "Add clientid auth rule"},
|
||||
{"clientid update <Username> <NewPassword>", "Update clientid auth rule"},
|
||||
{"clientid delete <Username>", "Delete clientid auth rule"}]).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Auth Username Cli
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
auth_username_cli(["list"]) ->
|
||||
[emqx_ctl:print("~s~n", [Username])
|
||||
|| {?TABLE, {username, Username}, _Password, _CreatedAt} <- all_users(username)
|
||||
];
|
||||
|
||||
auth_username_cli(["add", Username, Password]) ->
|
||||
case add_user({username, iolist_to_binary(Username)}, iolist_to_binary(Password)) of
|
||||
ok -> emqx_ctl:print("ok~n");
|
||||
{error, Reason} -> emqx_ctl:print("Error: ~p~n", [Reason])
|
||||
end;
|
||||
|
||||
auth_username_cli(["update", Username, NewPassword]) ->
|
||||
case update_user({username, iolist_to_binary(Username)}, iolist_to_binary(NewPassword)) of
|
||||
ok -> emqx_ctl:print("ok~n");
|
||||
{error, Reason} -> emqx_ctl:print("Error: ~p~n", [Reason])
|
||||
end;
|
||||
auth_username_cli(["del", Username]) ->
|
||||
auth_username_cli(["delete", Username]);
|
||||
|
||||
auth_username_cli(["delete", Username]) ->
|
||||
case remove_user({username, iolist_to_binary(Username)}) of
|
||||
ok -> emqx_ctl:print("ok~n");
|
||||
{error, Reason} -> emqx_ctl:print("Error: ~p~n", [Reason])
|
||||
end;
|
||||
|
||||
auth_username_cli(_) ->
|
||||
emqx_ctl:usage([{"user list", "List username auth rules"},
|
||||
{"user add <Username> <Password>", "Add username auth rule"},
|
||||
{"user update <Username> <NewPassword>", "Update username auth rule"},
|
||||
{"user delete <Username>", "Delete username auth rule"}]).
|
|
@ -1,36 +0,0 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_auth_mnesia_sup).
|
||||
|
||||
-behaviour(supervisor).
|
||||
|
||||
-include("emqx_auth_mnesia.hrl").
|
||||
|
||||
-export([start_link/0]).
|
||||
|
||||
%% Supervisor callbacks
|
||||
-export([init/1]).
|
||||
|
||||
start_link() ->
|
||||
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Supervisor callbacks
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
init([]) ->
|
||||
{ok, {{one_for_one, 10, 100}, []}}.
|
|
@ -1,293 +0,0 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_acl_mnesia_SUITE).
|
||||
|
||||
-compile(nowarn_export_all).
|
||||
-compile(export_all).
|
||||
|
||||
-include("emqx_auth_mnesia.hrl").
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
-include_lib("common_test/include/ct.hrl").
|
||||
|
||||
-import(emqx_ct_http, [ request_api/3
|
||||
, request_api/5
|
||||
, get_http_data/1
|
||||
, create_default_app/0
|
||||
, delete_default_app/0
|
||||
, default_auth_header/0
|
||||
]).
|
||||
|
||||
-define(HOST, "http://127.0.0.1:8081/").
|
||||
-define(API_VERSION, "v4").
|
||||
-define(BASE_PATH, "api").
|
||||
|
||||
all() ->
|
||||
emqx_ct:all(?MODULE).
|
||||
|
||||
groups() ->
|
||||
[].
|
||||
|
||||
init_per_suite(Config) ->
|
||||
emqx_ct_helpers:start_apps([emqx_modules, emqx_management, emqx_auth_mnesia], fun set_special_configs/1),
|
||||
create_default_app(),
|
||||
Config.
|
||||
|
||||
end_per_suite(_Config) ->
|
||||
delete_default_app(),
|
||||
emqx_ct_helpers:stop_apps([emqx_modules, emqx_management, emqx_auth_mnesia]).
|
||||
|
||||
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_management(_Config) ->
|
||||
clean_all_acls(),
|
||||
?assertEqual("Acl with Mnesia", emqx_acl_mnesia:description()),
|
||||
?assertEqual([], emqx_acl_mnesia_cli:all_acls()),
|
||||
|
||||
ok = emqx_acl_mnesia_cli:add_acl({clientid, <<"test_clientid">>}, <<"topic/%c">>, sub, allow),
|
||||
ok = emqx_acl_mnesia_cli:add_acl({clientid, <<"test_clientid">>}, <<"topic/+">>, pub, deny),
|
||||
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),
|
||||
ok = emqx_acl_mnesia_cli:add_acl(all, <<"#">>, pubsub, deny),
|
||||
%% Sleeps below are needed to hide the race condition between
|
||||
%% mnesia and ets dirty select in check_acl, that make this test
|
||||
%% flaky
|
||||
timer:sleep(100),
|
||||
|
||||
?assertEqual(2, length(emqx_acl_mnesia_cli:lookup_acl({clientid, <<"test_clientid">>}))),
|
||||
?assertEqual(2, length(emqx_acl_mnesia_cli:lookup_acl({username, <<"test_username">>}))),
|
||||
?assertEqual(2, length(emqx_acl_mnesia_cli:lookup_acl(all))),
|
||||
?assertEqual(6, length(emqx_acl_mnesia_cli:all_acls())),
|
||||
|
||||
User1 = #{zone => external, clientid => <<"test_clientid">>},
|
||||
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">>),
|
||||
|
||||
%% Test merging of pubsub capability:
|
||||
ok = emqx_acl_mnesia_cli:add_acl({clientid, <<"test_clientid">>}, <<"topic/mix">>, pubsub, deny),
|
||||
timer:sleep(100),
|
||||
deny = emqx_access_control:check_acl(User1, subscribe, <<"topic/mix">>),
|
||||
deny = emqx_access_control:check_acl(User1, publish, <<"topic/mix">>),
|
||||
ok = emqx_acl_mnesia_cli:add_acl({clientid, <<"test_clientid">>}, <<"topic/mix">>, pub, allow),
|
||||
timer:sleep(100),
|
||||
deny = emqx_access_control:check_acl(User1, subscribe, <<"topic/mix">>),
|
||||
allow = emqx_access_control:check_acl(User1, publish, <<"topic/mix">>),
|
||||
ok = emqx_acl_mnesia_cli:add_acl({clientid, <<"test_clientid">>}, <<"topic/mix">>, pubsub, allow),
|
||||
timer:sleep(100),
|
||||
allow = emqx_access_control:check_acl(User1, subscribe, <<"topic/mix">>),
|
||||
allow = emqx_access_control:check_acl(User1, publish, <<"topic/mix">>),
|
||||
ok = emqx_acl_mnesia_cli:add_acl({clientid, <<"test_clientid">>}, <<"topic/mix">>, sub, deny),
|
||||
timer:sleep(100),
|
||||
deny = emqx_access_control:check_acl(User1, subscribe, <<"topic/mix">>),
|
||||
allow = emqx_access_control:check_acl(User1, publish, <<"topic/mix">>),
|
||||
ok = emqx_acl_mnesia_cli:add_acl({clientid, <<"test_clientid">>}, <<"topic/mix">>, pub, deny),
|
||||
timer:sleep(100),
|
||||
deny = emqx_access_control:check_acl(User1, subscribe, <<"topic/mix">>),
|
||||
deny = emqx_access_control:check_acl(User1, publish, <<"topic/mix">>),
|
||||
|
||||
%% Test implicit migration of pubsub to pub and sub:
|
||||
ok = emqx_acl_mnesia_cli:remove_acl({clientid, <<"test_clientid">>}, <<"topic/mix">>),
|
||||
ok = mnesia:dirty_write(#emqx_acl{
|
||||
filter = {{clientid, <<"test_clientid">>}, <<"topic/mix">>},
|
||||
action = pubsub,
|
||||
access = allow,
|
||||
created_at = erlang:system_time(millisecond)
|
||||
}),
|
||||
timer:sleep(100),
|
||||
allow = emqx_access_control:check_acl(User1, subscribe, <<"topic/mix">>),
|
||||
allow = emqx_access_control:check_acl(User1, publish, <<"topic/mix">>),
|
||||
ok = emqx_acl_mnesia_cli:add_acl({clientid, <<"test_clientid">>}, <<"topic/mix">>, pub, deny),
|
||||
timer:sleep(100),
|
||||
allow = emqx_access_control:check_acl(User1, subscribe, <<"topic/mix">>),
|
||||
deny = emqx_access_control:check_acl(User1, publish, <<"topic/mix">>),
|
||||
ok = emqx_acl_mnesia_cli:add_acl({clientid, <<"test_clientid">>}, <<"topic/mix">>, sub, deny),
|
||||
timer:sleep(100),
|
||||
deny = emqx_access_control:check_acl(User1, subscribe, <<"topic/mix">>),
|
||||
deny = emqx_access_control:check_acl(User1, publish, <<"topic/mix">>),
|
||||
|
||||
ok = emqx_acl_mnesia_cli:remove_acl({clientid, <<"test_clientid">>}, <<"topic/%c">>),
|
||||
ok = emqx_acl_mnesia_cli:remove_acl({clientid, <<"test_clientid">>}, <<"topic/+">>),
|
||||
ok = emqx_acl_mnesia_cli:remove_acl({clientid, <<"test_clientid">>}, <<"topic/mix">>),
|
||||
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, <<"#">>),
|
||||
timer:sleep(100),
|
||||
|
||||
?assertEqual([], emqx_acl_mnesia_cli:all_acls()).
|
||||
|
||||
t_acl_cli(_Config) ->
|
||||
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),
|
||||
|
||||
clean_all_acls(),
|
||||
|
||||
?assertEqual(0, length(emqx_acl_mnesia_cli:cli(["list"]))),
|
||||
|
||||
emqx_acl_mnesia_cli:cli(["add", "clientid", "test_clientid", "topic/A", "pub", "deny"]),
|
||||
emqx_acl_mnesia_cli:cli(["add", "clientid", "test_clientid", "topic/A", "pub", "allow"]),
|
||||
R1 = emqx_ctl:format("Acl(clientid = ~p topic = ~p action = ~p access = ~p)~n",
|
||||
[<<"test_clientid">>, <<"topic/A">>, pub, allow]),
|
||||
?assertEqual([R1], emqx_acl_mnesia_cli:cli(["show", "clientid", "test_clientid"])),
|
||||
?assertEqual([R1], emqx_acl_mnesia_cli:cli(["list", "clientid"])),
|
||||
|
||||
emqx_acl_mnesia_cli:cli(["add", "username", "test_username", "topic/B", "sub", "deny"]),
|
||||
R2 = emqx_ctl:format("Acl(username = ~p topic = ~p action = ~p access = ~p)~n",
|
||||
[<<"test_username">>, <<"topic/B">>, sub, deny]),
|
||||
?assertEqual([R2], emqx_acl_mnesia_cli:cli(["show", "username", "test_username"])),
|
||||
?assertEqual([R2], emqx_acl_mnesia_cli:cli(["list", "username"])),
|
||||
|
||||
emqx_acl_mnesia_cli:cli(["add", "_all", "#", "pub", "allow"]),
|
||||
emqx_acl_mnesia_cli:cli(["add", "_all", "#", "pubsub", "deny"]),
|
||||
?assertMatch(["",
|
||||
"Acl($all topic = <<\"#\">> action = pub access = deny)",
|
||||
"Acl($all topic = <<\"#\">> action = sub access = deny)"],
|
||||
lists:sort(string:split(emqx_acl_mnesia_cli:cli(["list", "_all"]), "\n", all))
|
||||
),
|
||||
?assertEqual(4, 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).
|
||||
|
||||
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(4, 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(4, 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(4, 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
|
||||
%%------------------------------------------------------------------------------
|
||||
|
||||
clean_all_acls() ->
|
||||
[ mnesia:dirty_delete({emqx_acl, Login})
|
||||
|| Login <- mnesia:dirty_all_keys(emqx_acl)].
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% HTTP Request
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
request_http_rest_list(Path) ->
|
||||
request_api(get, uri(Path), default_auth_header()).
|
||||
|
||||
request_http_rest_lookup(Path) ->
|
||||
request_api(get, uri(Path), default_auth_header()).
|
||||
|
||||
request_http_rest_add(Path, Params) ->
|
||||
request_api(post, uri(Path), [], default_auth_header(), Params).
|
||||
|
||||
request_http_rest_delete(Path) ->
|
||||
request_api(delete, uri(Path), default_auth_header()).
|
||||
|
||||
uri() -> uri([]).
|
||||
uri(Parts) when is_list(Parts) ->
|
||||
NParts = [b2l(E) || E <- Parts],
|
||||
?HOST ++ filename:join([?BASE_PATH, ?API_VERSION, "acl"| NParts]).
|
||||
|
||||
b2l(B) -> binary_to_list(emqx_http_lib:uri_encode(iolist_to_binary(B))).
|
|
@ -1,318 +0,0 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_auth_mnesia_SUITE).
|
||||
|
||||
-compile(nowarn_export_all).
|
||||
-compile(export_all).
|
||||
|
||||
-include("emqx_auth_mnesia.hrl").
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
-include_lib("common_test/include/ct.hrl").
|
||||
|
||||
-import(emqx_ct_http, [ request_api/3
|
||||
, request_api/5
|
||||
, get_http_data/1
|
||||
, create_default_app/0
|
||||
, delete_default_app/0
|
||||
, default_auth_header/0
|
||||
]).
|
||||
|
||||
-define(HOST, "http://127.0.0.1:8081/").
|
||||
-define(API_VERSION, "v4").
|
||||
-define(BASE_PATH, "api").
|
||||
|
||||
-define(TABLE, emqx_user).
|
||||
-define(CLIENTID, <<"clientid_for_ct">>).
|
||||
-define(USERNAME, <<"username_for_ct">>).
|
||||
-define(PASSWORD, <<"password">>).
|
||||
-define(NPASSWORD, <<"new_password">>).
|
||||
|
||||
all() ->
|
||||
emqx_ct:all(?MODULE).
|
||||
|
||||
groups() ->
|
||||
[].
|
||||
|
||||
init_per_suite(Config) ->
|
||||
ok = emqx_ct_helpers:start_apps([emqx_management, emqx_auth_mnesia], fun set_special_configs/1),
|
||||
create_default_app(),
|
||||
Config.
|
||||
|
||||
end_per_suite(_Config) ->
|
||||
delete_default_app(),
|
||||
emqx_ct_helpers:stop_apps([emqx_management, emqx_auth_mnesia]).
|
||||
|
||||
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_management(_Config) ->
|
||||
clean_all_users(),
|
||||
|
||||
ok = emqx_auth_mnesia_cli:add_user({username, ?USERNAME}, ?PASSWORD),
|
||||
{error, existed} = emqx_auth_mnesia_cli:add_user({username, ?USERNAME}, ?PASSWORD),
|
||||
?assertMatch([{?TABLE, {username, ?USERNAME}, _, _}],
|
||||
emqx_auth_mnesia_cli:all_users(username)
|
||||
),
|
||||
|
||||
ok = emqx_auth_mnesia_cli:add_user({clientid, ?CLIENTID}, ?PASSWORD),
|
||||
{error, existed} = emqx_auth_mnesia_cli:add_user({clientid, ?CLIENTID}, ?PASSWORD),
|
||||
?assertMatch([{?TABLE, {clientid, ?CLIENTID}, _, _}],
|
||||
emqx_auth_mnesia_cli:all_users(clientid)
|
||||
),
|
||||
|
||||
?assertEqual(2, length(emqx_auth_mnesia_cli:all_users())),
|
||||
|
||||
ok = emqx_auth_mnesia_cli:update_user({username, ?USERNAME}, ?NPASSWORD),
|
||||
{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}, _, _}],
|
||||
emqx_auth_mnesia_cli:lookup_user({username, ?USERNAME})
|
||||
),
|
||||
?assertMatch([{?TABLE, {clientid, ?CLIENTID}, _, _}],
|
||||
emqx_auth_mnesia_cli:lookup_user({clientid, ?CLIENTID})
|
||||
),
|
||||
|
||||
User1 = #{username => ?USERNAME,
|
||||
clientid => undefined,
|
||||
password => ?NPASSWORD,
|
||||
zone => external},
|
||||
|
||||
{ok, #{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({username, ?USERNAME}),
|
||||
{ok, #{auth_result := success,
|
||||
anonymous := true }} = emqx_access_control:authenticate(User1),
|
||||
|
||||
User2 = #{clientid => ?CLIENTID,
|
||||
password => ?NPASSWORD,
|
||||
zone => external},
|
||||
|
||||
{ok, #{auth_result := success,
|
||||
anonymous := false}} = emqx_access_control:authenticate(User2),
|
||||
|
||||
{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(),
|
||||
|
||||
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),
|
||||
|
||||
Params1 = #{<<"clientid">> => ?CLIENTID, <<"password">> => ?PASSWORD},
|
||||
{ok, _} = request_http_rest_add(["auth_clientid"], Params1),
|
||||
|
||||
Path = ["auth_clientid/" ++ binary_to_list(?CLIENTID)],
|
||||
Params2 = #{<<"clientid">> => ?CLIENTID, <<"password">> => ?NPASSWORD},
|
||||
{ok, _} = request_http_rest_update(Path, Params2),
|
||||
|
||||
{ok, Result2} = request_http_rest_lookup(Path),
|
||||
?assertMatch(#{<<"clientid">> := ?CLIENTID}, get_http_data(Result2)),
|
||||
|
||||
Params3 = [ #{<<"clientid">> => ?CLIENTID, <<"password">> => ?PASSWORD}
|
||||
, #{<<"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, Result4} = request_http_rest_list(["auth_clientid"]),
|
||||
?assertEqual(3, length(get_http_data(Result4))),
|
||||
|
||||
{ok, _} = request_http_rest_delete(Path),
|
||||
{ok, Result5} = request_http_rest_lookup(Path),
|
||||
?assertMatch(#{}, get_http_data(Result5)).
|
||||
|
||||
t_username_rest_api(_Config) ->
|
||||
clean_all_users(),
|
||||
|
||||
{ok, Result1} = request_http_rest_list(["auth_username"]),
|
||||
[] = get_http_data(Result1),
|
||||
|
||||
Params1 = #{<<"username">> => ?USERNAME, <<"password">> => ?PASSWORD},
|
||||
{ok, _} = request_http_rest_add(["auth_username"], Params1),
|
||||
|
||||
Path = ["auth_username/" ++ binary_to_list(?USERNAME)],
|
||||
Params2 = #{<<"username">> => ?USERNAME, <<"password">> => ?NPASSWORD},
|
||||
{ok, _} = request_http_rest_update(Path, Params2),
|
||||
|
||||
{ok, Result2} = request_http_rest_lookup(Path),
|
||||
?assertMatch(#{<<"username">> := ?USERNAME}, get_http_data(Result2)),
|
||||
|
||||
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)),
|
||||
|
||||
{ok, Result4} = request_http_rest_list(["auth_username"]),
|
||||
?assertEqual(3, length(get_http_data(Result4))),
|
||||
|
||||
{ok, _} = request_http_rest_delete(Path),
|
||||
{ok, Result5} = request_http_rest_lookup([Path]),
|
||||
?assertMatch(#{}, get_http_data(Result5)).
|
||||
|
||||
t_password_hash(_) ->
|
||||
clean_all_users(),
|
||||
{ok, Default} = application:get_env(emqx_auth_mnesia, password_hash),
|
||||
application:set_env(emqx_auth_mnesia, password_hash, plain),
|
||||
|
||||
%% change the password_hash to 'plain'
|
||||
application:stop(emqx_auth_mnesia),
|
||||
ok = application:start(emqx_auth_mnesia),
|
||||
|
||||
Params = #{<<"username">> => ?USERNAME, <<"password">> => ?PASSWORD},
|
||||
{ok, _} = request_http_rest_add(["auth_username"], Params),
|
||||
|
||||
%% check
|
||||
User = #{username => ?USERNAME,
|
||||
clientid => undefined,
|
||||
password => ?PASSWORD,
|
||||
zone => external},
|
||||
{ok, #{auth_result := success,
|
||||
anonymous := false}} = emqx_access_control:authenticate(User),
|
||||
|
||||
application:set_env(emqx_auth_mnesia, password_hash, Default),
|
||||
application:stop(emqx_auth_mnesia),
|
||||
ok = application:start(emqx_auth_mnesia).
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
%% Helpers
|
||||
%%------------------------------------------------------------------------------
|
||||
|
||||
clean_all_users() ->
|
||||
[ mnesia:dirty_delete({emqx_user, Login})
|
||||
|| Login <- mnesia:dirty_all_keys(emqx_user)].
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% HTTP Request
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
request_http_rest_list(Path) ->
|
||||
request_api(get, uri(Path), default_auth_header()).
|
||||
|
||||
request_http_rest_lookup(Path) ->
|
||||
request_api(get, uri([Path]), default_auth_header()).
|
||||
|
||||
request_http_rest_add(Path, Params) ->
|
||||
request_api(post, uri(Path), [], default_auth_header(), Params).
|
||||
|
||||
request_http_rest_update(Path, Params) ->
|
||||
request_api(put, uri([Path]), [], default_auth_header(), Params).
|
||||
|
||||
request_http_rest_delete(Login) ->
|
||||
request_api(delete, uri([Login]), default_auth_header()).
|
||||
|
||||
uri() -> uri([]).
|
||||
uri(Parts) when is_list(Parts) ->
|
||||
NParts = [b2l(E) || E <- Parts],
|
||||
?HOST ++ filename:join([?BASE_PATH, ?API_VERSION | NParts]).
|
||||
|
||||
%% @private
|
||||
b2l(B) when is_binary(B) ->
|
||||
binary_to_list(B);
|
||||
b2l(L) when is_list(L) ->
|
||||
L.
|
|
@ -1,24 +0,0 @@
|
|||
.eunit
|
||||
deps
|
||||
*.o
|
||||
*.beam
|
||||
*.plt
|
||||
erl_crash.dump
|
||||
ebin
|
||||
rel/example_project
|
||||
.concrete/DEV_MODE
|
||||
.rebar
|
||||
.DS_Store
|
||||
.erlang.mk/
|
||||
emqx_auth_mongo.d
|
||||
ct.coverdata
|
||||
logs/
|
||||
test/ct.cover.spec
|
||||
data/
|
||||
cover/
|
||||
eunit.coverdata
|
||||
_build/
|
||||
rebar.lock
|
||||
erlang.mk
|
||||
etc/emqx_auth_mongo.conf.rendered
|
||||
.rebar3
|
|
@ -1,31 +0,0 @@
|
|||
|
||||
2.0.7 (2017-01-20)
|
||||
------------------
|
||||
|
||||
Tag 2.0.7 - use `cuttlefish:unset()` for commented ACL/super config
|
||||
|
||||
2.0.1 (2016-11-30)
|
||||
------------------
|
||||
|
||||
Tag 2.0.1
|
||||
|
||||
2.0-beta.1 (2016-08-24)
|
||||
-----------------------
|
||||
|
||||
gen_conf
|
||||
|
||||
1.1.3-beta (2016-08-19)
|
||||
-----------------------
|
||||
|
||||
Bump version to 1.1.3
|
||||
|
||||
1.1.2-beta (2016-06-30)
|
||||
-----------------------
|
||||
|
||||
Bump version to 1.1.2
|
||||
|
||||
1.1-beta (2016-05-28)
|
||||
---------------------
|
||||
|
||||
First public release
|
||||
|
|
@ -1,192 +0,0 @@
|
|||
emqx_auth_mongo
|
||||
===============
|
||||
|
||||
EMQ X Authentication/ACL with MongoDB
|
||||
|
||||
Build the Plugin
|
||||
----------------
|
||||
|
||||
```
|
||||
make & make tests
|
||||
```
|
||||
|
||||
Configuration
|
||||
-------------
|
||||
|
||||
File: etc/emqx_auth_mongo.conf
|
||||
|
||||
```
|
||||
## MongoDB Topology Type.
|
||||
##
|
||||
## Value: single | unknown | sharded | rs
|
||||
auth.mongo.type = single
|
||||
|
||||
## Sets the set name if type is rs.
|
||||
##
|
||||
## Value: String
|
||||
## auth.mongo.rs_set_name =
|
||||
|
||||
## MongoDB server list.
|
||||
##
|
||||
## Value: String
|
||||
##
|
||||
## Examples: 127.0.0.1:27017,127.0.0.2:27017...
|
||||
auth.mongo.server = 127.0.0.1:27017
|
||||
|
||||
## MongoDB pool size
|
||||
##
|
||||
## Value: Number
|
||||
auth.mongo.pool = 8
|
||||
|
||||
## MongoDB login user.
|
||||
##
|
||||
## Value: String
|
||||
## auth.mongo.login =
|
||||
|
||||
## MongoDB password.
|
||||
##
|
||||
## Value: String
|
||||
## auth.mongo.password =
|
||||
|
||||
## MongoDB AuthSource
|
||||
##
|
||||
## Value: String
|
||||
## Default: mqtt
|
||||
## auth.mongo.auth_source = admin
|
||||
|
||||
## MongoDB database
|
||||
##
|
||||
## Value: String
|
||||
auth.mongo.database = mqtt
|
||||
|
||||
## MongoDB write mode.
|
||||
##
|
||||
## Value: unsafe | safe
|
||||
## auth.mongo.w_mode =
|
||||
|
||||
## Mongo read mode.
|
||||
##
|
||||
## Value: master | slave_ok
|
||||
## auth.mongo.r_mode =
|
||||
|
||||
## MongoDB topology options.
|
||||
auth.mongo.topology.pool_size = 1
|
||||
auth.mongo.topology.max_overflow = 0
|
||||
## auth.mongo.topology.overflow_ttl = 1000
|
||||
## auth.mongo.topology.overflow_check_period = 1000
|
||||
## auth.mongo.topology.local_threshold_ms = 1000
|
||||
## auth.mongo.topology.connect_timeout_ms = 20000
|
||||
## auth.mongo.topology.socket_timeout_ms = 100
|
||||
## auth.mongo.topology.server_selection_timeout_ms = 30000
|
||||
## auth.mongo.topology.wait_queue_timeout_ms = 1000
|
||||
## auth.mongo.topology.heartbeat_frequency_ms = 10000
|
||||
## auth.mongo.topology.min_heartbeat_frequency_ms = 1000
|
||||
|
||||
## Authentication query.
|
||||
auth.mongo.auth_query.collection = mqtt_user
|
||||
|
||||
auth.mongo.auth_query.password_field = password
|
||||
|
||||
## Password hash.
|
||||
##
|
||||
## Value: plain | md5 | sha | sha256 | bcrypt
|
||||
auth.mongo.auth_query.password_hash = sha256
|
||||
|
||||
## sha256 with salt suffix
|
||||
## auth.mongo.auth_query.password_hash = sha256,salt
|
||||
|
||||
## sha256 with salt prefix
|
||||
## auth.mongo.auth_query.password_hash = salt,sha256
|
||||
|
||||
## bcrypt with salt prefix
|
||||
## auth.mongo.auth_query.password_hash = salt,bcrypt
|
||||
|
||||
## pbkdf2 with macfun iterations dklen
|
||||
## macfun: md4, md5, ripemd160, sha, sha224, sha256, sha384, sha512
|
||||
## auth.mongo.auth_query.password_hash = pbkdf2,sha256,1000,20
|
||||
|
||||
auth.mongo.auth_query.selector = username=%u
|
||||
|
||||
## Enable superuser query.
|
||||
auth.mongo.super_query = on
|
||||
|
||||
auth.mongo.super_query.collection = mqtt_user
|
||||
|
||||
auth.mongo.super_query.super_field = is_superuser
|
||||
|
||||
auth.mongo.super_query.selector = username=%u
|
||||
|
||||
## Enable ACL query.
|
||||
auth.mongo.acl_query = on
|
||||
|
||||
auth.mongo.acl_query.collection = mqtt_acl
|
||||
|
||||
auth.mongo.acl_query.selector = username=%u
|
||||
```
|
||||
|
||||
Load the Plugin
|
||||
---------------
|
||||
|
||||
```
|
||||
./bin/emqx_ctl plugins load emqx_auth_mongo
|
||||
```
|
||||
|
||||
MongoDB Database
|
||||
----------------
|
||||
|
||||
```
|
||||
use mqtt
|
||||
db.createCollection("mqtt_user")
|
||||
db.createCollection("mqtt_acl")
|
||||
db.mqtt_user.ensureIndex({"username":1})
|
||||
```
|
||||
|
||||
mqtt_user Collection
|
||||
--------------------
|
||||
|
||||
```
|
||||
{
|
||||
username: "user",
|
||||
password: "password hash",
|
||||
salt: "password salt",
|
||||
is_superuser: boolean (true, false),
|
||||
created: "datetime"
|
||||
}
|
||||
```
|
||||
|
||||
For example:
|
||||
```
|
||||
db.mqtt_user.insert({username: "test", password: "password hash", salt: "password salt", is_superuser: false})
|
||||
db.mqtt_user.insert({username: "root", is_superuser: true})
|
||||
```
|
||||
|
||||
mqtt_acl Collection
|
||||
-------------------
|
||||
|
||||
```
|
||||
{
|
||||
username: "username",
|
||||
clientid: "clientid",
|
||||
publish: ["topic1", "topic2", ...],
|
||||
subscribe: ["subtop1", "subtop2", ...],
|
||||
pubsub: ["topic/#", "topic1", ...]
|
||||
}
|
||||
```
|
||||
|
||||
For example:
|
||||
|
||||
```
|
||||
db.mqtt_acl.insert({username: "test", publish: ["t/1", "t/2"], subscribe: ["user/%u", "client/%c"]})
|
||||
db.mqtt_acl.insert({username: "admin", pubsub: ["#"]})
|
||||
```
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
Apache License Version 2.0
|
||||
|
||||
Author
|
||||
------
|
||||
|
||||
EMQ X Team.
|
||||
|
|
@ -1,187 +0,0 @@
|
|||
##--------------------------------------------------------------------
|
||||
## MongoDB Auth/ACL Plugin
|
||||
##--------------------------------------------------------------------
|
||||
|
||||
## MongoDB Topology Type.
|
||||
##
|
||||
## Value: single | unknown | sharded | rs
|
||||
auth.mongo.type = single
|
||||
|
||||
## The set name if type is rs.
|
||||
##
|
||||
## Value: String
|
||||
## auth.mongo.rs_set_name =
|
||||
|
||||
## MongoDB server list.
|
||||
##
|
||||
## Value: String
|
||||
##
|
||||
## Examples: "127.0.0.1:27017,127.0.0.2:27017,..."
|
||||
auth.mongo.server = "127.0.0.1:27017"
|
||||
|
||||
## MongoDB pool size
|
||||
##
|
||||
## Value: Number
|
||||
auth.mongo.pool = 8
|
||||
|
||||
## MongoDB login user.
|
||||
##
|
||||
## Value: String
|
||||
# auth.mongo.username =
|
||||
|
||||
## MongoDB password.
|
||||
##
|
||||
## Value: String
|
||||
## auth.mongo.password =
|
||||
|
||||
## MongoDB AuthSource
|
||||
##
|
||||
## Value: String
|
||||
## Default: mqtt
|
||||
## auth.mongo.auth_source = admin
|
||||
|
||||
## MongoDB database
|
||||
##
|
||||
## Value: String
|
||||
auth.mongo.database = mqtt
|
||||
|
||||
## MongoDB query timeout
|
||||
##
|
||||
## Value: Duration
|
||||
## auth.mongo.query_timeout = 5s
|
||||
|
||||
## Whether to enable SSL connection.
|
||||
##
|
||||
## Value: on | off
|
||||
## auth.mongo.ssl.enable = off
|
||||
|
||||
## SSL keyfile.
|
||||
##
|
||||
## Value: File
|
||||
## auth.mongo.ssl.keyfile =
|
||||
|
||||
## SSL certfile.
|
||||
##
|
||||
## Value: File
|
||||
## auth.mongo.ssl.certfile =
|
||||
|
||||
## SSL cacertfile.
|
||||
##
|
||||
## Value: File
|
||||
## auth.mongo.ssl.cacertfile =
|
||||
|
||||
## In mode verify_none the default behavior is to allow all x509-path
|
||||
## validation errors.
|
||||
##
|
||||
## Value: true | false
|
||||
## auth.mongo.ssl.verify = false
|
||||
|
||||
## If not specified, the server's names returned in server's certificate is validated against
|
||||
## what's provided `auth.mongo.server` config's host part.
|
||||
## Setting to 'disable' will make EMQ X ignore unmatched server names.
|
||||
## If set with a host name, the server's names returned in server's certificate is validated
|
||||
## against this value.
|
||||
##
|
||||
## Value: String | disable
|
||||
## auth.mongo.ssl.server_name_indication = disable
|
||||
|
||||
## MongoDB write mode.
|
||||
##
|
||||
## Value: unsafe | safe
|
||||
## auth.mongo.w_mode =
|
||||
|
||||
## Mongo read mode.
|
||||
##
|
||||
## Value: master | slave_ok
|
||||
## auth.mongo.r_mode =
|
||||
|
||||
## MongoDB topology options.
|
||||
auth.mongo.topology.pool_size = 1
|
||||
auth.mongo.topology.max_overflow = 0
|
||||
## auth.mongo.topology.overflow_ttl = 1000
|
||||
## auth.mongo.topology.overflow_check_period = 1000
|
||||
## auth.mongo.topology.local_threshold_ms = 1000
|
||||
## auth.mongo.topology.connect_timeout_ms = 20000
|
||||
## auth.mongo.topology.socket_timeout_ms = 100
|
||||
## auth.mongo.topology.server_selection_timeout_ms = 30000
|
||||
## auth.mongo.topology.wait_queue_timeout_ms = 1000
|
||||
## auth.mongo.topology.heartbeat_frequency_ms = 10000
|
||||
## auth.mongo.topology.min_heartbeat_frequency_ms = 1000
|
||||
|
||||
## -------------------------------------------------
|
||||
## Auth Query
|
||||
## -------------------------------------------------
|
||||
## Password hash.
|
||||
##
|
||||
## Value: plain | md5 | sha | sha256 | bcrypt
|
||||
auth.mongo.auth_query.password_hash = sha256
|
||||
|
||||
## sha256 with salt suffix
|
||||
## auth.mongo.auth_query.password_hash = "sha256,salt"
|
||||
|
||||
## sha256 with salt prefix
|
||||
## auth.mongo.auth_query.password_hash = "salt,sha256"
|
||||
|
||||
## bcrypt with salt prefix
|
||||
## auth.mongo.auth_query.password_hash = "salt,bcrypt"
|
||||
|
||||
## pbkdf2 with macfun iterations dklen
|
||||
## macfun: md4, md5, ripemd160, sha, sha224, sha256, sha384, sha512
|
||||
## auth.mongo.auth_query.password_hash = "pbkdf2,sha256,1000,20"
|
||||
|
||||
## Authentication query.
|
||||
auth.mongo.auth_query.collection = mqtt_user
|
||||
|
||||
## Password mainly fields
|
||||
##
|
||||
## Value: password | password,salt
|
||||
auth.mongo.auth_query.password_field = password
|
||||
|
||||
## Authentication Selector.
|
||||
##
|
||||
## Variables:
|
||||
## - %u: username
|
||||
## - %c: clientid
|
||||
## - %C: common name of client TLS cert
|
||||
## - %d: subject of client TLS cert
|
||||
##
|
||||
## auth.mongo.auth_query.selector = {Field}={Placeholder}
|
||||
auth.mongo.auth_query.selector = "username=%u"
|
||||
|
||||
## -------------------------------------------------
|
||||
## Super User Query
|
||||
## -------------------------------------------------
|
||||
auth.mongo.super_query.collection = mqtt_user
|
||||
auth.mongo.super_query.super_field = is_superuser
|
||||
#auth.mongo.super_query.selector.1 = username=%u, clientid=%c
|
||||
auth.mongo.super_query.selector = "username=%u"
|
||||
|
||||
## ACL Selector.
|
||||
##
|
||||
## Multiple selectors could be combined with '$or'
|
||||
## when query acl from mongo.
|
||||
##
|
||||
## e.g.
|
||||
##
|
||||
## With following 2 selectors configured:
|
||||
##
|
||||
## auth.mongo.acl_query.selector.1 = "username=%u"
|
||||
## auth.mongo.acl_query.selector.2 = "username=$all"
|
||||
##
|
||||
## And if a client connected using username 'ilyas',
|
||||
## then the following mongo command will be used to
|
||||
## retrieve acl entries:
|
||||
##
|
||||
## db.mqtt_acl.find({$or: [{username: "ilyas"}, {username: "$all"}]});
|
||||
##
|
||||
## Variables:
|
||||
## - %u: username
|
||||
## - %c: clientid
|
||||
##
|
||||
## Examples:
|
||||
##
|
||||
## auth.mongo.acl_query.selector.1 = "username=%u,clientid=%c"
|
||||
## auth.mongo.acl_query.selector.2 = "username=$all"
|
||||
## auth.mongo.acl_query.selector.3 = "clientid=$all"
|
||||
auth.mongo.acl_query.collection = mqtt_acl
|
||||
auth.mongo.acl_query.selector = "username=%u"
|
|
@ -1,37 +0,0 @@
|
|||
|
||||
-define(APP, emqx_auth_mongo).
|
||||
|
||||
-define(DEFAULT_SELECTORS, [{<<"username">>, <<"%u">>}]).
|
||||
|
||||
-record(superquery, {collection = <<"mqtt_user">>,
|
||||
field = <<"is_superuser">>,
|
||||
selector = {<<"username">>, <<"%u">>}}).
|
||||
|
||||
-record(authquery, {collection = <<"mqtt_user">>,
|
||||
field = <<"password">>,
|
||||
hash = sha256,
|
||||
selector = {<<"username">>, <<"%u">>}}).
|
||||
|
||||
-record(aclquery, {collection = <<"mqtt_acl">>,
|
||||
selector = {<<"username">>, <<"%u">>}}).
|
||||
|
||||
-record(auth_metrics, {
|
||||
success = 'client.auth.success',
|
||||
failure = 'client.auth.failure',
|
||||
ignore = 'client.auth.ignore'
|
||||
}).
|
||||
|
||||
-record(acl_metrics, {
|
||||
allow = 'client.acl.allow',
|
||||
deny = 'client.acl.deny',
|
||||
ignore = 'client.acl.ignore'
|
||||
}).
|
||||
|
||||
-define(METRICS(Type), tl(tuple_to_list(#Type{}))).
|
||||
-define(METRICS(Type, K), #Type{}#Type.K).
|
||||
|
||||
-define(AUTH_METRICS, ?METRICS(auth_metrics)).
|
||||
-define(AUTH_METRICS(K), ?METRICS(auth_metrics, K)).
|
||||
|
||||
-define(ACL_METRICS, ?METRICS(acl_metrics)).
|
||||
-define(ACL_METRICS(K), ?METRICS(acl_metrics, K)).
|
|
@ -1,341 +0,0 @@
|
|||
%%-*- mode: erlang -*-
|
||||
%% emqx_auth_mongo config mapping
|
||||
|
||||
{mapping, "auth.mongo.type", "emqx_auth_mongo.server", [
|
||||
{default, single},
|
||||
{datatype, {enum, [single, unknown, sharded, rs]}}
|
||||
]}.
|
||||
|
||||
{mapping, "auth.mongo.rs_set_name", "emqx_auth_mongo.server", [
|
||||
{default, "mqtt"},
|
||||
{datatype, string}
|
||||
]}.
|
||||
|
||||
{mapping, "auth.mongo.server", "emqx_auth_mongo.server", [
|
||||
{default, "127.0.0.1:27017"},
|
||||
{datatype, string}
|
||||
]}.
|
||||
|
||||
{mapping, "auth.mongo.pool", "emqx_auth_mongo.server", [
|
||||
{default, 8},
|
||||
{datatype, integer}
|
||||
]}.
|
||||
|
||||
%% FIXME: compatible with 4.0-4.2 version format, plan to delete in 5.0
|
||||
{mapping, "auth.mongo.login", "emqx_auth_mongo.server", [
|
||||
{datatype, string}
|
||||
]}.
|
||||
|
||||
{mapping, "auth.mongo.username", "emqx_auth_mongo.server", [
|
||||
{datatype, string}
|
||||
]}.
|
||||
|
||||
{mapping, "auth.mongo.password", "emqx_auth_mongo.server", [
|
||||
{default, ""},
|
||||
{datatype, string}
|
||||
]}.
|
||||
|
||||
{mapping, "auth.mongo.database", "emqx_auth_mongo.server", [
|
||||
{default, "mqtt"},
|
||||
{datatype, string}
|
||||
]}.
|
||||
|
||||
{mapping, "auth.mongo.auth_source", "emqx_auth_mongo.server", [
|
||||
{default, "mqtt"},
|
||||
{datatype, string}
|
||||
]}.
|
||||
|
||||
{mapping, "auth.mongo.ssl.enable", "emqx_auth_mongo.server", [
|
||||
{default, off},
|
||||
{datatype, {enum, [on, off, true, false]}} %% FIXME: ture/false is compatible with 4.0-4.2 version format, plan to delete in 5.0
|
||||
]}.
|
||||
|
||||
{mapping, "auth.mongo.ssl.keyfile", "emqx_auth_mongo.server", [
|
||||
{datatype, string}
|
||||
]}.
|
||||
|
||||
{mapping, "auth.mongo.ssl.certfile", "emqx_auth_mongo.server", [
|
||||
{datatype, string}
|
||||
]}.
|
||||
|
||||
{mapping, "auth.mongo.ssl.cacertfile", "emqx_auth_mongo.server", [
|
||||
{datatype, string}
|
||||
]}.
|
||||
|
||||
{mapping, "auth.mongo.ssl.verify", "emqx_auth_mongo.server", [
|
||||
{default, false},
|
||||
{datatype, {enum, [true, false]}}
|
||||
]}.
|
||||
|
||||
{mapping, "auth.mongo.ssl.server_name_indication", "emqx_auth_mongo.server", [
|
||||
{datatype, string}
|
||||
]}.
|
||||
|
||||
%% FIXME: compatible with 4.0-4.2 version format, plan to delete in 5.0
|
||||
{mapping, "auth.mongo.ssl_opts.keyfile", "emqx_auth_mongo.server", [
|
||||
{datatype, string}
|
||||
]}.
|
||||
|
||||
%% FIXME: compatible with 4.0-4.2 version format, plan to delete in 5.0
|
||||
{mapping, "auth.mongo.ssl_opts.certfile", "emqx_auth_mongo.server", [
|
||||
{datatype, string}
|
||||
]}.
|
||||
|
||||
%% FIXME: compatible with 4.0-4.2 version format, plan to delete in 5.0
|
||||
{mapping, "auth.mongo.ssl_opts.cacertfile", "emqx_auth_mongo.server", [
|
||||
{datatype, string}
|
||||
]}.
|
||||
|
||||
{mapping, "auth.mongo.w_mode", "emqx_auth_mongo.server", [
|
||||
{default, undef},
|
||||
{datatype, {enum, [safe, unsafe, undef]}}
|
||||
]}.
|
||||
|
||||
{mapping, "auth.mongo.r_mode", "emqx_auth_mongo.server", [
|
||||
{default, undef},
|
||||
{datatype, {enum, [master, slave_ok, undef]}}
|
||||
]}.
|
||||
|
||||
{mapping, "auth.mongo.topology.$name", "emqx_auth_mongo.server", [
|
||||
{datatype, integer}
|
||||
]}.
|
||||
|
||||
{translation, "emqx_auth_mongo.server", fun(Conf) ->
|
||||
H = cuttlefish:conf_get("auth.mongo.server", Conf),
|
||||
Hosts = string:tokens(H, ","),
|
||||
Type0 = cuttlefish:conf_get("auth.mongo.type", Conf),
|
||||
Pool = cuttlefish:conf_get("auth.mongo.pool", Conf),
|
||||
%% FIXME: compatible with 4.0-4.2 version format, plan to delete in 5.0
|
||||
Login = cuttlefish:conf_get("auth.mongo.username", Conf,
|
||||
cuttlefish:conf_get("auth.mongo.login", Conf, "")
|
||||
),
|
||||
Passwd = cuttlefish:conf_get("auth.mongo.password", Conf),
|
||||
DB = cuttlefish:conf_get("auth.mongo.database", Conf),
|
||||
AuthSrc = cuttlefish:conf_get("auth.mongo.auth_source", Conf),
|
||||
R = cuttlefish:conf_get("auth.mongo.w_mode", Conf),
|
||||
W = cuttlefish:conf_get("auth.mongo.r_mode", Conf),
|
||||
Login0 = case Login =:= [] of
|
||||
true -> [];
|
||||
false -> [{login, list_to_binary(Login)}]
|
||||
end,
|
||||
Passwd0 = case Passwd =:= [] of
|
||||
true -> [];
|
||||
false -> [{password, list_to_binary(Passwd)}]
|
||||
end,
|
||||
W0 = case W =:= undef of
|
||||
true -> [];
|
||||
false -> [{w_mode, W}]
|
||||
end,
|
||||
R0 = case R =:= undef of
|
||||
true -> [];
|
||||
false -> [{r_mode, R}]
|
||||
end,
|
||||
Filter = fun(Opts) -> [{K, V} || {K, V} <- Opts, V =/= undefined] end,
|
||||
SslOpts = fun(Prefix) ->
|
||||
Verify = case cuttlefish:conf_get(Prefix ++ ".verify", Conf, false) of
|
||||
true -> verify_peer;
|
||||
false -> verify_none
|
||||
end,
|
||||
Filter([{verify, Verify},
|
||||
{server_name_indication, case cuttlefish:conf_get(Prefix ++ ".server_name_indication", Conf, undefined) of
|
||||
"disable" -> disable;
|
||||
SNI -> SNI
|
||||
end},
|
||||
{keyfile, cuttlefish:conf_get(Prefix ++ ".keyfile", Conf, undefined)},
|
||||
{certfile, cuttlefish:conf_get(Prefix ++ ".certfile", Conf, undefined)},
|
||||
{cacertfile, cuttlefish:conf_get(Prefix ++ ".cacertfile", Conf, undefined)}])
|
||||
end,
|
||||
|
||||
%% FIXME: compatible with 4.0-4.2 version format, plan to delete in 5.0
|
||||
GenSsl = case cuttlefish:conf_get("auth.mongo.ssl.cacertfile", Conf, undefined) of
|
||||
undefined -> [{ssl, true}, {ssl_opts, SslOpts("auth.mongo.ssl_opts")}];
|
||||
_ -> [{ssl, true}, {ssl_opts, SslOpts("auth.mongo.ssl")}]
|
||||
end,
|
||||
|
||||
%% FIXME: compatible with 4.0-4.2 version format, plan to delete in 5.0
|
||||
Ssl = case cuttlefish:conf_get("auth.mongo.ssl.enable", Conf) of
|
||||
on -> GenSsl;
|
||||
off -> [];
|
||||
true -> [{ssl, true}, {ssl_opts, SslOpts("auth.mongo.ssl_opts")}];
|
||||
false -> []
|
||||
end,
|
||||
|
||||
WorkerOptions = [{database, list_to_binary(DB)}, {auth_source, list_to_binary(AuthSrc)}]
|
||||
++ Login0 ++ Passwd0 ++ W0 ++ R0 ++ Ssl,
|
||||
|
||||
Vars = cuttlefish_variable:fuzzy_matches(["auth", "mongo", "topology", "$name"], Conf),
|
||||
Options = lists:map(fun({_, Name}) ->
|
||||
Name2 = case Name of
|
||||
"local_threshold_ms" -> "localThresholdMS";
|
||||
"connect_timeout_ms" -> "connectTimeoutMS";
|
||||
"socket_timeout_ms" -> "socketTimeoutMS";
|
||||
"server_selection_timeout_ms" -> "serverSelectionTimeoutMS";
|
||||
"wait_queue_timeout_ms" -> "waitQueueTimeoutMS";
|
||||
"heartbeat_frequency_ms" -> "heartbeatFrequencyMS";
|
||||
"min_heartbeat_frequency_ms" -> "minHeartbeatFrequencyMS";
|
||||
_ -> Name
|
||||
end,
|
||||
{list_to_atom(Name2), cuttlefish:conf_get("auth.mongo.topology."++Name, Conf)}
|
||||
end, Vars),
|
||||
|
||||
Type = case Type0 =:= rs of
|
||||
true -> {Type0, list_to_binary(cuttlefish:conf_get("auth.mongo.rs_set_name", Conf))};
|
||||
false -> Type0
|
||||
end,
|
||||
[{type, Type},
|
||||
{hosts, Hosts},
|
||||
{options, Options},
|
||||
{worker_options, WorkerOptions},
|
||||
{auto_reconnect, 1},
|
||||
{pool_size, Pool}]
|
||||
end}.
|
||||
|
||||
%% The mongodb operation timeout is specified by the value of `cursor_timeout` from application config,
|
||||
%% or `infinity` if `cursor_timeout` not specified
|
||||
{mapping, "auth.mongo.query_timeout", "mongodb.cursor_timeout", [
|
||||
{datatype, string}
|
||||
]}.
|
||||
|
||||
{translation, "mongodb.cursor_timeout", fun(Conf) ->
|
||||
case cuttlefish:conf_get("auth.mongo.query_timeout", Conf, undefined) of
|
||||
undefined -> infinity;
|
||||
Duration ->
|
||||
case cuttlefish_duration:parse(Duration, ms) of
|
||||
{error, Reason} -> error(Reason);
|
||||
Ms when is_integer(Ms) -> Ms
|
||||
end
|
||||
end
|
||||
end}.
|
||||
|
||||
{mapping, "auth.mongo.auth_query.collection", "emqx_auth_mongo.auth_query", [
|
||||
{default, "mqtt_user"},
|
||||
{datatype, string}
|
||||
]}.
|
||||
|
||||
{mapping, "auth.mongo.auth_query.password_field", "emqx_auth_mongo.auth_query", [
|
||||
{default, "password"},
|
||||
{datatype, string}
|
||||
]}.
|
||||
|
||||
{mapping, "auth.mongo.auth_query.password_hash", "emqx_auth_mongo.auth_query", [
|
||||
{datatype, string}
|
||||
]}.
|
||||
|
||||
{mapping, "auth.mongo.auth_query.selector", "emqx_auth_mongo.auth_query", [
|
||||
{default, ""},
|
||||
{datatype, string}
|
||||
]}.
|
||||
|
||||
{translation, "emqx_auth_mongo.auth_query", fun(Conf) ->
|
||||
case cuttlefish:conf_get("auth.mongo.auth_query.collection", Conf) of
|
||||
undefined -> cuttlefish:unset();
|
||||
Collection ->
|
||||
PasswordField = cuttlefish:conf_get("auth.mongo.auth_query.password_field", Conf),
|
||||
PasswordHash = cuttlefish:conf_get("auth.mongo.auth_query.password_hash", Conf),
|
||||
SelectorStr = cuttlefish:conf_get("auth.mongo.auth_query.selector", Conf),
|
||||
SelectorList =
|
||||
lists:map(fun(Selector) ->
|
||||
case string:tokens(Selector, "=") of
|
||||
[Field, Val] -> {list_to_binary(Field), list_to_binary(Val)};
|
||||
_ -> {<<"username">>, <<"%u">>}
|
||||
end
|
||||
end, string:tokens(SelectorStr, ", ")),
|
||||
|
||||
PasswordFields = [list_to_binary(Field) || Field <- string:tokens(PasswordField, ",")],
|
||||
HashValue =
|
||||
case string:tokens(PasswordHash, ",") of
|
||||
[Hash] -> list_to_atom(Hash);
|
||||
[Prefix, Suffix] -> {list_to_atom(Prefix), list_to_atom(Suffix)};
|
||||
[Hash, MacFun, Iterations, Dklen] -> {list_to_atom(Hash), list_to_atom(MacFun), list_to_integer(Iterations), list_to_integer(Dklen)};
|
||||
_ -> plain
|
||||
end,
|
||||
[{collection, Collection},
|
||||
{password_field, PasswordFields},
|
||||
{password_hash, HashValue},
|
||||
{selector, SelectorList}]
|
||||
end
|
||||
end}.
|
||||
|
||||
{mapping, "auth.mongo.super_query", "emqx_auth_mongo.super_query", [
|
||||
{default, off},
|
||||
{datatype, flag}
|
||||
]}.
|
||||
|
||||
{mapping, "auth.mongo.super_query.collection", "emqx_auth_mongo.super_query", [
|
||||
{default, "mqtt_user"},
|
||||
{datatype, string}
|
||||
]}.
|
||||
|
||||
{mapping, "auth.mongo.super_query.super_field", "emqx_auth_mongo.super_query", [
|
||||
{default, "is_superuser"},
|
||||
{datatype, string}
|
||||
]}.
|
||||
|
||||
{mapping, "auth.mongo.super_query.selector", "emqx_auth_mongo.super_query", [
|
||||
{default, ""},
|
||||
{datatype, string}
|
||||
]}.
|
||||
|
||||
{translation, "emqx_auth_mongo.super_query", fun(Conf) ->
|
||||
case cuttlefish:conf_get("auth.mongo.super_query.collection", Conf) of
|
||||
undefined -> cuttlefish:unset();
|
||||
Collection ->
|
||||
SuperField = cuttlefish:conf_get("auth.mongo.super_query.super_field", Conf),
|
||||
SelectorStr = cuttlefish:conf_get("auth.mongo.super_query.selector", Conf),
|
||||
SelectorList =
|
||||
lists:map(fun(Selector) ->
|
||||
case string:tokens(Selector, "=") of
|
||||
[Field, Val] -> {list_to_binary(Field), list_to_binary(Val)};
|
||||
_ -> {<<"username">>, <<"%u">>}
|
||||
end
|
||||
end, string:tokens(SelectorStr, ", ")),
|
||||
[{collection, Collection}, {super_field, SuperField}, {selector, SelectorList}]
|
||||
end
|
||||
end}.
|
||||
|
||||
{mapping, "auth.mongo.acl_query", "emqx_auth_mongo.acl_query", [
|
||||
{default, off},
|
||||
{datatype, flag}
|
||||
]}.
|
||||
|
||||
{mapping, "auth.mongo.acl_query.collection", "emqx_auth_mongo.acl_query", [
|
||||
{default, "mqtt_user"},
|
||||
{datatype, string}
|
||||
]}.
|
||||
|
||||
{mapping, "auth.mongo.acl_query.selector", "emqx_auth_mongo.acl_query", [
|
||||
{default, ""},
|
||||
{datatype, string}
|
||||
]}.
|
||||
{mapping, "auth.mongo.acl_query.selector.$id", "emqx_auth_mongo.acl_query", [
|
||||
{default, ""},
|
||||
{datatype, string}
|
||||
]}.
|
||||
|
||||
{translation, "emqx_auth_mongo.acl_query", fun(Conf) ->
|
||||
case cuttlefish:conf_get("auth.mongo.acl_query.collection", Conf) of
|
||||
undefined -> cuttlefish:unset();
|
||||
Collection ->
|
||||
SelectorStrList =
|
||||
lists:map(
|
||||
fun
|
||||
({["auth","mongo","acl_query","selector"], ConfEntry}) ->
|
||||
ConfEntry;
|
||||
({["auth","mongo","acl_query","selector", _], ConfEntry}) ->
|
||||
ConfEntry
|
||||
end,
|
||||
cuttlefish_variable:filter_by_prefix("auth.mongo.acl_query.selector", Conf)),
|
||||
SelectorListList =
|
||||
lists:map(
|
||||
fun(SelectorStr) ->
|
||||
lists:map(fun(Selector) ->
|
||||
case string:tokens(Selector, "=") of
|
||||
[Field, Val] -> {list_to_binary(Field), list_to_binary(Val)};
|
||||
_ -> {<<"username">>, <<"%u">>}
|
||||
end
|
||||
end, string:tokens(SelectorStr, ", "))
|
||||
end,
|
||||
SelectorStrList),
|
||||
[{collection, Collection}, {selector, SelectorListList}]
|
||||
end
|
||||
end}.
|
|
@ -1,32 +0,0 @@
|
|||
{deps,
|
||||
%% NOTE: mind poolboy version when updating mongodb-erlang version
|
||||
[{mongodb, {git,"https://github.com/emqx/mongodb-erlang", {tag, "v3.0.7"}}},
|
||||
%% mongodb-erlang uses a special fork https://github.com/comtihon/poolboy.git
|
||||
%% (which has overflow_ttl feature added).
|
||||
%% However, it references `{branch, "master}` (commit 9c06a9a on 2021-04-07).
|
||||
%% By accident, We have always been using the upstream fork due to
|
||||
%% eredis_cluster's dependency getting resolved earlier.
|
||||
%% Here we pin 1.5.2 to avoid surprises in the future.
|
||||
{poolboy, {git, "https://github.com/emqx/poolboy.git", {tag, "1.5.2"}}}
|
||||
]}.
|
||||
|
||||
{edoc_opts, [{preprocess, true}]}.
|
||||
{erl_opts, [warn_unused_vars,
|
||||
warn_shadow_vars,
|
||||
warn_unused_import,
|
||||
warn_obsolete_guard,
|
||||
debug_info,
|
||||
compressed,
|
||||
{parse_transform}
|
||||
]}.
|
||||
{overrides, [{add, [{erl_opts, [compressed]}]}]}.
|
||||
|
||||
{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}.
|
||||
|
|
@ -1,91 +0,0 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_acl_mongo).
|
||||
|
||||
-include("emqx_auth_mongo.hrl").
|
||||
-include_lib("emqx/include/emqx.hrl").
|
||||
-include_lib("emqx/include/logger.hrl").
|
||||
|
||||
%% ACL callbacks
|
||||
-export([ register_metrics/0
|
||||
, check_acl/5
|
||||
, description/0
|
||||
]).
|
||||
-spec(register_metrics() -> ok).
|
||||
register_metrics() ->
|
||||
lists:foreach(fun emqx_metrics:ensure/1, ?ACL_METRICS).
|
||||
|
||||
check_acl(#{username := <<$$, _/binary>>}, _PubSub, _Topic, _AclResult, _State) ->
|
||||
ok;
|
||||
|
||||
check_acl(ClientInfo, PubSub, Topic, _AclResult, Env = #{aclquery := AclQuery}) ->
|
||||
#aclquery{collection = Coll, selector = SelectorList} = AclQuery,
|
||||
Pool = maps:get(pool, Env, ?APP),
|
||||
SelectorMapList =
|
||||
lists:map(fun(Selector) ->
|
||||
maps:from_list(emqx_auth_mongo:replvars(Selector, ClientInfo))
|
||||
end, SelectorList),
|
||||
case emqx_auth_mongo:query_multi(Pool, Coll, SelectorMapList) of
|
||||
[] -> ok;
|
||||
Rows ->
|
||||
try match(ClientInfo, Topic, topics(PubSub, Rows)) of
|
||||
matched -> emqx_metrics:inc(?ACL_METRICS(allow)),
|
||||
{stop, allow};
|
||||
nomatch -> emqx_metrics:inc(?ACL_METRICS(deny)),
|
||||
{stop, deny}
|
||||
catch
|
||||
_Err:Reason->
|
||||
?LOG(error, "[MongoDB] Check mongo ~p ACL failed, got ACL config: ~p, error: :~p",
|
||||
[PubSub, Rows, Reason]),
|
||||
emqx_metrics:inc(?ACL_METRICS(ignore)),
|
||||
ignore
|
||||
end
|
||||
end.
|
||||
|
||||
|
||||
match(_ClientInfo, _Topic, []) ->
|
||||
nomatch;
|
||||
match(ClientInfo, Topic, [TopicFilter|More]) ->
|
||||
case emqx_topic:match(Topic, feedvar(ClientInfo, TopicFilter)) of
|
||||
true -> matched;
|
||||
false -> match(ClientInfo, Topic, More)
|
||||
end.
|
||||
|
||||
topics(publish, Rows) ->
|
||||
lists:foldl(fun(Row, Acc) ->
|
||||
Topics = maps:get(<<"publish">>, Row, []) ++ maps:get(<<"pubsub">>, Row, []),
|
||||
lists:umerge(Acc, Topics)
|
||||
end, [], Rows);
|
||||
|
||||
topics(subscribe, Rows) ->
|
||||
lists:foldl(fun(Row, Acc) ->
|
||||
Topics = maps:get(<<"subscribe">>, Row, []) ++ maps:get(<<"pubsub">>, Row, []),
|
||||
lists:umerge(Acc, Topics)
|
||||
end, [], Rows).
|
||||
|
||||
feedvar(#{clientid := ClientId, username := Username}, Str) ->
|
||||
lists:foldl(fun({Var, Val}, Acc) ->
|
||||
feedvar(Acc, Var, Val)
|
||||
end, Str, [{"%u", Username}, {"%c", ClientId}]).
|
||||
|
||||
feedvar(Str, _Var, undefined) ->
|
||||
Str;
|
||||
feedvar(Str, Var, Val) ->
|
||||
re:replace(Str, Var, Val, [global, {return, binary}]).
|
||||
|
||||
description() -> "ACL with MongoDB".
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
{application, emqx_auth_mongo,
|
||||
[{description, "EMQ X Authentication/ACL with MongoDB"},
|
||||
{vsn, "4.4.0"}, % strict semver, bump manually!
|
||||
{modules, []},
|
||||
{registered, [emqx_auth_mongo_sup]},
|
||||
{applications, [kernel,stdlib,mongodb,ecpool]},
|
||||
{mod, {emqx_auth_mongo_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-mongo"}
|
||||
]}
|
||||
]}.
|
|
@ -1,138 +0,0 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_auth_mongo).
|
||||
|
||||
-behaviour(ecpool_worker).
|
||||
|
||||
-include("emqx_auth_mongo.hrl").
|
||||
-include_lib("emqx/include/emqx.hrl").
|
||||
-include_lib("emqx/include/logger.hrl").
|
||||
-include_lib("emqx/include/types.hrl").
|
||||
|
||||
-export([ register_metrics/0
|
||||
, check/3
|
||||
, description/0
|
||||
]).
|
||||
|
||||
-export([ replvar/2
|
||||
, replvars/2
|
||||
, connect/1
|
||||
, query/3
|
||||
, query_multi/3
|
||||
]).
|
||||
|
||||
-spec(register_metrics() -> ok).
|
||||
register_metrics() ->
|
||||
lists:foreach(fun emqx_metrics:ensure/1, ?AUTH_METRICS).
|
||||
|
||||
check(ClientInfo = #{password := Password}, AuthResult,
|
||||
Env = #{authquery := AuthQuery, superquery := SuperQuery}) ->
|
||||
#authquery{collection = Collection, field = Fields,
|
||||
hash = HashType, selector = Selector} = AuthQuery,
|
||||
Pool = maps:get(pool, Env, ?APP),
|
||||
case query(Pool, Collection, maps:from_list(replvars(Selector, ClientInfo))) of
|
||||
undefined -> emqx_metrics:inc(?AUTH_METRICS(ignore));
|
||||
{error, Reason} ->
|
||||
?LOG(error, "[MongoDB] Can't connect to MongoDB server: ~0p", [Reason]),
|
||||
ok = emqx_metrics:inc(?AUTH_METRICS(failure)),
|
||||
{stop, AuthResult#{auth_result => not_authorized, anonymous => false}};
|
||||
UserMap ->
|
||||
Result = case [maps:get(Field, UserMap, undefined) || Field <- Fields] of
|
||||
[undefined] -> {error, password_error};
|
||||
[PassHash] ->
|
||||
check_pass({PassHash, Password}, HashType);
|
||||
[PassHash, Salt|_] ->
|
||||
check_pass({PassHash, Salt, Password}, HashType)
|
||||
end,
|
||||
case Result of
|
||||
ok ->
|
||||
ok = emqx_metrics:inc(?AUTH_METRICS(success)),
|
||||
{stop, AuthResult#{is_superuser => is_superuser(Pool, SuperQuery, ClientInfo),
|
||||
anonymous => false,
|
||||
auth_result => success}};
|
||||
{error, Error} ->
|
||||
?LOG(error, "[MongoDB] check auth fail: ~p", [Error]),
|
||||
ok = emqx_metrics:inc(?AUTH_METRICS(failure)),
|
||||
{stop, AuthResult#{auth_result => Error, anonymous => false}}
|
||||
end
|
||||
end.
|
||||
|
||||
check_pass(Password, HashType) ->
|
||||
case emqx_passwd:check_pass(Password, HashType) of
|
||||
ok -> ok;
|
||||
{error, _Reason} -> {error, not_authorized}
|
||||
end.
|
||||
|
||||
description() -> "Authentication with MongoDB".
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Is Superuser?
|
||||
%%--------------------------------------------------------------------
|
||||
is_superuser(_Pool, undefined, _ClientInfo) ->
|
||||
false;
|
||||
is_superuser(Pool, #superquery{collection = Coll, field = Field, selector = Selector}, ClientInfo) ->
|
||||
case query(Pool, Coll, maps:from_list(replvars(Selector, ClientInfo))) of
|
||||
undefined -> false;
|
||||
{error, Reason} ->
|
||||
?LOG(error, "[MongoDB] Can't connect to MongoDB server: ~0p", [Reason]),
|
||||
false;
|
||||
Row ->
|
||||
case maps:get(Field, Row, false) of
|
||||
true -> true;
|
||||
_False -> false
|
||||
end
|
||||
end.
|
||||
|
||||
replvars(VarList, ClientInfo) ->
|
||||
lists:map(fun(Var) -> replvar(Var, ClientInfo) end, VarList).
|
||||
|
||||
replvar({Field, <<"%u">>}, #{username := Username}) ->
|
||||
{Field, Username};
|
||||
replvar({Field, <<"%c">>}, #{clientid := ClientId}) ->
|
||||
{Field, ClientId};
|
||||
replvar({Field, <<"%C">>}, #{cn := CN}) ->
|
||||
{Field, CN};
|
||||
replvar({Field, <<"%d">>}, #{dn := DN}) ->
|
||||
{Field, DN};
|
||||
replvar(Selector, _ClientInfo) ->
|
||||
Selector.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% MongoDB Connect/Query
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
connect(Opts) ->
|
||||
Type = proplists:get_value(type, Opts, single),
|
||||
Hosts = proplists:get_value(hosts, Opts, []),
|
||||
Options = proplists:get_value(options, Opts, []),
|
||||
WorkerOptions = proplists:get_value(worker_options, Opts, []),
|
||||
mongo_api:connect(Type, Hosts, Options, WorkerOptions).
|
||||
|
||||
query(Pool, Collection, Selector) ->
|
||||
ecpool:with_client(Pool, fun(Conn) -> mongo_api:find_one(Conn, Collection, Selector, #{}) end).
|
||||
|
||||
query_multi(Pool, Collection, SelectorList) ->
|
||||
lists:reverse(lists:flatten(lists:foldl(fun(Selector, Acc1) ->
|
||||
Batch = ecpool:with_client(Pool, fun(Conn) ->
|
||||
case mongo_api:find(Conn, Collection, Selector, #{}) of
|
||||
[] -> [];
|
||||
{ok, Cursor} ->
|
||||
mc_cursor:foldl(fun(O, Acc2) -> [O|Acc2] end, [], Cursor, 1000)
|
||||
end
|
||||
end),
|
||||
[Batch|Acc1]
|
||||
end, [], SelectorList))).
|
|
@ -1,88 +0,0 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_auth_mongo_app).
|
||||
|
||||
-behaviour(application).
|
||||
|
||||
-emqx_plugin(auth).
|
||||
|
||||
-include("emqx_auth_mongo.hrl").
|
||||
|
||||
-import(proplists, [get_value/3]).
|
||||
|
||||
%% Application callbacks
|
||||
-export([ start/2
|
||||
, prep_stop/1
|
||||
, stop/1
|
||||
]).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Application callbacks
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
start(_StartType, _StartArgs) ->
|
||||
{ok, Sup} = emqx_auth_mongo_sup:start_link(),
|
||||
with_env(auth_query, fun reg_authmod/1),
|
||||
with_env(acl_query, fun reg_aclmod/1),
|
||||
{ok, Sup}.
|
||||
|
||||
prep_stop(State) ->
|
||||
ok = emqx:unhook('client.authenticate', {emqx_auth_mongo, check}),
|
||||
ok = emqx:unhook('client.check_acl', {emqx_acl_mongo, check_acl}),
|
||||
State.
|
||||
|
||||
stop(_State) ->
|
||||
ok.
|
||||
|
||||
reg_authmod(AuthQuery) ->
|
||||
emqx_auth_mongo:register_metrics(),
|
||||
SuperQuery = r(super_query, application:get_env(?APP, super_query, undefined)),
|
||||
ok = emqx:hook('client.authenticate', {emqx_auth_mongo, check,
|
||||
[#{authquery => AuthQuery, superquery => SuperQuery, pool => ?APP}]
|
||||
}).
|
||||
|
||||
reg_aclmod(AclQuery) ->
|
||||
emqx_acl_mongo:register_metrics(),
|
||||
ok = emqx:hook('client.check_acl', {emqx_acl_mongo, check_acl, [#{aclquery => AclQuery, pool => ?APP}]}).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Internal functions
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
with_env(Name, Fun) ->
|
||||
case application:get_env(?APP, Name) of
|
||||
undefined -> ok;
|
||||
{ok, Config} -> Fun(r(Name, Config))
|
||||
end.
|
||||
|
||||
r(super_query, undefined) ->
|
||||
undefined;
|
||||
r(super_query, Config) ->
|
||||
#superquery{collection = list_to_binary(get_value(collection, Config, "mqtt_user")),
|
||||
field = list_to_binary(get_value(super_field, Config, "is_superuser")),
|
||||
selector = get_value(selector, Config, ?DEFAULT_SELECTORS)};
|
||||
|
||||
r(auth_query, Config) ->
|
||||
#authquery{collection = list_to_binary(get_value(collection, Config, "mqtt_user")),
|
||||
field = get_value(password_field, Config, [<<"password">>]),
|
||||
hash = get_value(password_hash, Config, sha256),
|
||||
selector = get_value(selector, Config, ?DEFAULT_SELECTORS)};
|
||||
|
||||
r(acl_query, Config) ->
|
||||
#aclquery{collection = list_to_binary(get_value(collection, Config, "mqtt_acl")),
|
||||
selector = get_value(selector, Config, [?DEFAULT_SELECTORS])}.
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_auth_mongo_sup).
|
||||
|
||||
-behaviour(supervisor).
|
||||
|
||||
-include("emqx_auth_mongo.hrl").
|
||||
|
||||
-export([start_link/0]).
|
||||
|
||||
-export([init/1]).
|
||||
|
||||
start_link() ->
|
||||
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
|
||||
|
||||
init([]) ->
|
||||
{ok, PoolEnv} = application:get_env(?APP, server),
|
||||
PoolSpec = ecpool:pool_spec(?APP, ?APP, ?APP, PoolEnv),
|
||||
{ok, {{one_for_all, 10, 100}, [PoolSpec]}}.
|
||||
|
|
@ -1,169 +0,0 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_auth_mongo_SUITE).
|
||||
|
||||
-compile(export_all).
|
||||
-compile(nowarn_export_all).
|
||||
|
||||
-include_lib("emqx/include/emqx.hrl").
|
||||
-include_lib("common_test/include/ct.hrl").
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
|
||||
-define(APP, emqx_auth_mongo).
|
||||
|
||||
-define(POOL(App), ecpool_worker:client(gproc_pool:pick_worker({ecpool, App}))).
|
||||
|
||||
-define(MONGO_CL_ACL, <<"mqtt_acl">>).
|
||||
-define(MONGO_CL_USER, <<"mqtt_user">>).
|
||||
|
||||
-define(INIT_ACL, [{<<"username">>, <<"testuser">>, <<"clientid">>, <<"null">>, <<"subscribe">>, [<<"#">>]},
|
||||
{<<"username">>, <<"dashboard">>, <<"clientid">>, <<"null">>, <<"pubsub">>, [<<"$SYS/#">>]},
|
||||
{<<"username">>, <<"user3">>, <<"clientid">>, <<"null">>, <<"publish">>, [<<"a/b/c">>]}]).
|
||||
|
||||
-define(INIT_AUTH, [{<<"username">>, <<"plain">>, <<"password">>, <<"plain">>, <<"salt">>, <<"salt">>, <<"is_superuser">>, true},
|
||||
{<<"username">>, <<"md5">>, <<"password">>, <<"1bc29b36f623ba82aaf6724fd3b16718">>, <<"salt">>, <<"salt">>, <<"is_superuser">>, false},
|
||||
{<<"username">>, <<"sha">>, <<"password">>, <<"d8f4590320e1343a915b6394170650a8f35d6926">>, <<"salt">>, <<"salt">>, <<"is_superuser">>, false},
|
||||
{<<"username">>, <<"sha256">>, <<"password">>, <<"5d5b09f6dcb2d53a5fffc60c4ac0d55fabdf556069d6631545f42aa6e3500f2e">>, <<"salt">>, <<"salt">>, <<"is_superuser">>, false},
|
||||
{<<"username">>, <<"pbkdf2_password">>, <<"password">>, <<"cdedb5281bb2f801565a1122b2563515">>, <<"salt">>, <<"ATHENA.MIT.EDUraeburn">>, <<"is_superuser">>, false},
|
||||
{<<"username">>, <<"bcrypt_foo">>, <<"password">>, <<"$2a$12$sSS8Eg.ovVzaHzi1nUHYK.HbUIOdlQI0iS22Q5rd5z.JVVYH6sfm6">>, <<"salt">>, <<"$2a$12$sSS8Eg.ovVzaHzi1nUHYK.">>, <<"is_superuser">>, false}
|
||||
]).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Setups
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
all() ->
|
||||
emqx_ct:all(?MODULE).
|
||||
|
||||
init_per_suite(Cfg) ->
|
||||
emqx_ct_helpers:start_apps([emqx_auth_mongo], fun set_special_confs/1),
|
||||
init_mongo_data(),
|
||||
Cfg.
|
||||
|
||||
end_per_suite(_Cfg) ->
|
||||
deinit_mongo_data(),
|
||||
emqx_ct_helpers:stop_apps([emqx_auth_mongo]).
|
||||
|
||||
set_special_confs(emqx) ->
|
||||
application:set_env(emqx, acl_nomatch, deny),
|
||||
application:set_env(emqx, allow_anonymous, false),
|
||||
application:set_env(emqx, enable_acl_cache, false);
|
||||
set_special_confs(_App) ->
|
||||
ok.
|
||||
|
||||
init_mongo_data() ->
|
||||
%% Users
|
||||
{ok, Connection} = ?POOL(?APP),
|
||||
mongo_api:delete(Connection, ?MONGO_CL_USER, {}),
|
||||
?assertMatch({{true, _}, _}, mongo_api:insert(Connection, ?MONGO_CL_USER, ?INIT_AUTH)),
|
||||
%% ACLs
|
||||
mongo_api:delete(Connection, ?MONGO_CL_ACL, {}),
|
||||
?assertMatch({{true, _}, _}, mongo_api:insert(Connection, ?MONGO_CL_ACL, ?INIT_ACL)).
|
||||
|
||||
deinit_mongo_data() ->
|
||||
{ok, Connection} = ?POOL(?APP),
|
||||
mongo_api:delete(Connection, ?MONGO_CL_USER, {}),
|
||||
mongo_api:delete(Connection, ?MONGO_CL_ACL, {}).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Test cases
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
t_check_auth(_) ->
|
||||
Plain = #{zone => external, clientid => <<"client1">>, username => <<"plain">>},
|
||||
Plain1 = #{zone => external, clientid => <<"client1">>, username => <<"plain2">>},
|
||||
Md5 = #{zone => external, clientid => <<"md5">>, username => <<"md5">>},
|
||||
Sha = #{zone => external, clientid => <<"sha">>, username => <<"sha">>},
|
||||
Sha256 = #{zone => external, clientid => <<"sha256">>, username => <<"sha256">>},
|
||||
Pbkdf2 = #{zone => external, clientid => <<"pbkdf2_password">>, username => <<"pbkdf2_password">>},
|
||||
Bcrypt = #{zone => external, clientid => <<"bcrypt_foo">>, username => <<"bcrypt_foo">>},
|
||||
User1 = #{zone => external, clientid => <<"bcrypt_foo">>, username => <<"user">>},
|
||||
reload({auth_query, [{password_hash, plain}]}),
|
||||
%% With exactly username/password, connection success
|
||||
{ok, #{is_superuser := true}} = emqx_access_control:authenticate(Plain#{password => <<"plain">>}),
|
||||
%% With exactly username and wrong password, connection fail
|
||||
{error, _} = emqx_access_control:authenticate(Plain#{password => <<"error_pwd">>}),
|
||||
%% With wrong username and wrong password, emqx_auth_mongo auth fail, then allow anonymous authentication
|
||||
{error, _} = emqx_access_control:authenticate(Plain1#{password => <<"error_pwd">>}),
|
||||
%% With wrong username and exactly password, emqx_auth_mongo auth fail, then allow anonymous authentication
|
||||
{error, _} = emqx_access_control:authenticate(Plain1#{password => <<"plain">>}),
|
||||
reload({auth_query, [{password_hash, md5}]}),
|
||||
{ok, #{is_superuser := false}} = emqx_access_control:authenticate(Md5#{password => <<"md5">>}),
|
||||
reload({auth_query, [{password_hash, sha}]}),
|
||||
{ok, #{is_superuser := false}} = emqx_access_control:authenticate(Sha#{password => <<"sha">>}),
|
||||
reload({auth_query, [{password_hash, sha256}]}),
|
||||
{ok, #{is_superuser := false}} = emqx_access_control:authenticate(Sha256#{password => <<"sha256">>}),
|
||||
%%pbkdf2 sha
|
||||
reload({auth_query, [{password_hash, {pbkdf2, sha, 1, 16}}, {password_field, [<<"password">>, <<"salt">>]}]}),
|
||||
{ok, #{is_superuser := false}} = emqx_access_control:authenticate(Pbkdf2#{password => <<"password">>}),
|
||||
reload({auth_query, [{password_hash, {salt, bcrypt}}]}),
|
||||
{ok, #{is_superuser := false}} = emqx_access_control:authenticate(Bcrypt#{password => <<"foo">>}),
|
||||
{error, _} = emqx_access_control:authenticate(User1#{password => <<"foo">>}).
|
||||
|
||||
t_check_acl(_) ->
|
||||
{ok, Connection} = ?POOL(?APP),
|
||||
User1 = #{zone => external, clientid => <<"client1">>, username => <<"testuser">>},
|
||||
User2 = #{zone => external, clientid => <<"client2">>, username => <<"dashboard">>},
|
||||
User3 = #{zone => external, clientid => <<"client2">>, username => <<"user3">>},
|
||||
User4 = #{zone => external, clientid => <<"$$client2">>, username => <<"$$user3">>},
|
||||
3 = mongo_api:count(Connection, ?MONGO_CL_ACL, {}, 17),
|
||||
%% ct log output
|
||||
allow = emqx_access_control:check_acl(User1, subscribe, <<"users/testuser/1">>),
|
||||
deny = emqx_access_control:check_acl(User1, subscribe, <<"$SYS/testuser/1">>),
|
||||
deny = emqx_access_control:check_acl(User2, subscribe, <<"a/b/c">>),
|
||||
allow = emqx_access_control:check_acl(User2, subscribe, <<"$SYS/testuser/1">>),
|
||||
allow = emqx_access_control:check_acl(User3, publish, <<"a/b/c">>),
|
||||
deny = emqx_access_control:check_acl(User3, publish, <<"c">>),
|
||||
deny = emqx_access_control:check_acl(User4, publish, <<"a/b/c">>).
|
||||
|
||||
t_acl_super(_) ->
|
||||
reload({auth_query, [{password_hash, plain}, {password_field, [<<"password">>]}]}),
|
||||
{ok, C} = emqtt:start_link([{clientid, <<"simpleClient">>},
|
||||
{username, <<"plain">>},
|
||||
{password, <<"plain">>}]),
|
||||
{ok, _} = emqtt:connect(C),
|
||||
timer:sleep(10),
|
||||
emqtt:subscribe(C, <<"TopicA">>, qos2),
|
||||
timer:sleep(1000),
|
||||
emqtt:publish(C, <<"TopicA">>, <<"Payload">>, qos2),
|
||||
timer:sleep(1000),
|
||||
receive
|
||||
{publish, #{payload := Payload}} ->
|
||||
?assertEqual(<<"Payload">>, Payload)
|
||||
after
|
||||
1000 ->
|
||||
ct:fail({receive_timeout, <<"Payload">>}),
|
||||
ok
|
||||
end,
|
||||
emqtt:disconnect(C).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Utils
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
reload({Par, Vals}) when is_list(Vals) ->
|
||||
application:stop(?APP),
|
||||
{ok, TupleVals} = application:get_env(?APP, Par),
|
||||
NewVals =
|
||||
lists:filtermap(fun({K, V}) ->
|
||||
case lists:keymember(K, 1, Vals) of
|
||||
false ->{true, {K, V}};
|
||||
_ -> false
|
||||
end
|
||||
end, TupleVals),
|
||||
application:set_env(?APP, Par, lists:append(NewVals, Vals)),
|
||||
application:start(?APP).
|
|
@ -1,27 +0,0 @@
|
|||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEpAIBAAKCAQEA0kGUBi9NDp65jgdxKfizIfuSr2wpwb44yM9SuP4oUQSULOA2
|
||||
4iFpLR/c5FAYHU81y9Vx91dQjdZfffaBZuv2zVvteXUkol8Nez7boKbo2E41MTew
|
||||
8edtNKZAQVvnaHAC2NCZxjchCzUCDEoUUcl+cIERZ8R48FBqK5iTVcMRIx1akwus
|
||||
+dhBqP0ykA5TGOWZkJrLM9aUXSPQha9+wXlOpkvu0Ur2nkX8PPJnifWao9UShSar
|
||||
ll1IqPZNCSlZMwcFYcQNBCpdvITUUYlHvMRQV64bUpOxUGDuJkQL3dLKBlNuBRlJ
|
||||
BcjBAKw7rFnwwHZcMmQ9tan/dZzpzwjo/T0XjwIDAQABAoIBAQCSHvUqnzDkWjcG
|
||||
l/Fzg92qXlYBCCC0/ugj1sHcwvVt6Mq5rVE3MpUPwTcYjPlVVTlD4aEEjm/zQuq2
|
||||
ddxUlOS+r4aIhHrjRT/vSS4FpjnoKeIZxGR6maVxk6DQS3i1QjMYT1CvSpzyVvKH
|
||||
a+xXMrtmoKxh+085ZAmFJtIuJhUA2yEa4zggCxWnvz8ecLClUPfVDPhdLBHc3KmL
|
||||
CRpHEC6L/wanvDPRdkkzfKyaJuIJlTDaCg63AY5sDkTW2I57iI/nJ3haSeidfQKz
|
||||
39EfbnM1A/YprIakafjAu3frBIsjBVcxwGihZmL/YriTHjOggJF841kT5zFkkv2L
|
||||
/530Wk6xAoGBAOqZLZ4DIi/zLndEOz1mRbUfjc7GQUdYplBnBwJ22VdS0P4TOXnd
|
||||
UbJth2MA92NM7ocTYVFl4TVIZY/Y+Prxk7KQdHWzR7JPpKfx9OEVgtSqV0vF9eGI
|
||||
rKp79Y1T4Mvc3UcQCXX6TP7nHLihEzpS8odm2LW4txrOiLsn4Fq/IWrLAoGBAOVv
|
||||
6U4tm3lImotUupKLZPKEBYwruo9qRysoug9FiorP4TjaBVOfltiiHbAQD6aGfVtN
|
||||
SZpZZtrs17wL7Xl4db5asgMcZd+8Hkfo5siR7AuGW9FZloOjDcXb5wCh9EvjJ74J
|
||||
Cjw7RqyVymq9t7IP6wnVwj5Ck48YhlOZCz/mzlnNAoGAWq7NYFgLvgc9feLFF23S
|
||||
IjpJQZWHJEITP98jaYNxbfzYRm49+GphqxwFinKULjFNvq7yHlnIXSVYBOu1CqOZ
|
||||
GRwXuGuNmlKI7lZr9xmukfAqgGLMMdr4C4qRF4lFyufcLRz42z7exmWlx4ST/yaT
|
||||
E13hBRWayeTuG5JFei6Jh1MCgYEAqmX4LyC+JFBgvvQZcLboLRkSCa18bADxhENG
|
||||
FAuAvmFvksqRRC71WETmqZj0Fqgxt7pp3KFjO1rFSprNLvbg85PmO1s+6fCLyLpX
|
||||
lESTu2d5D71qhK93jigooxalGitFm+SY3mzjq0/AOpBWOn+J/w7rqVPGxXLgaHv0
|
||||
l+vx+00CgYBOvo9/ImjwYii2jFl+sHEoCzlvpITi2temRlT2j6ulSjCLJgjwEFw9
|
||||
8e+vvfQumQOsutakUVyURrkMGNDiNlIv8kv5YLCCkrwN22E6Ghyi69MJUvHQXkc/
|
||||
QZhjn/luyfpB5f/BeHFS2bkkxAXo+cfG45ApY3Qfz6/7o+H+vDa6/A==
|
||||
-----END RSA PRIVATE KEY-----
|
|
@ -1,19 +0,0 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIDAzCCAeugAwIBAgIBATANBgkqhkiG9w0BAQsFADA8MTowOAYDVQQDDDFNeVNR
|
||||
TF9TZXJ2ZXJfOC4wLjE5X0F1dG9fR2VuZXJhdGVkX0NBX0NlcnRpZmljYXRlMB4X
|
||||
DTIwMDYxMTAzMzg0NloXDTMwMDYwOTAzMzg0NlowPDE6MDgGA1UEAwwxTXlTUUxf
|
||||
U2VydmVyXzguMC4xOV9BdXRvX0dlbmVyYXRlZF9DQV9DZXJ0aWZpY2F0ZTCCASIw
|
||||
DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANJBlAYvTQ6euY4HcSn4syH7kq9s
|
||||
KcG+OMjPUrj+KFEElCzgNuIhaS0f3ORQGB1PNcvVcfdXUI3WX332gWbr9s1b7Xl1
|
||||
JKJfDXs+26Cm6NhONTE3sPHnbTSmQEFb52hwAtjQmcY3IQs1AgxKFFHJfnCBEWfE
|
||||
ePBQaiuYk1XDESMdWpMLrPnYQaj9MpAOUxjlmZCayzPWlF0j0IWvfsF5TqZL7tFK
|
||||
9p5F/DzyZ4n1mqPVEoUmq5ZdSKj2TQkpWTMHBWHEDQQqXbyE1FGJR7zEUFeuG1KT
|
||||
sVBg7iZEC93SygZTbgUZSQXIwQCsO6xZ8MB2XDJkPbWp/3Wc6c8I6P09F48CAwEA
|
||||
AaMQMA4wDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEADKz6bIpP5anp
|
||||
GgLB0jkclRWuMlS4qqIt4itSsMXPJ/ezpHwECixmgW2TIQl6S1woRkUeMxhT2/Ay
|
||||
Sn/7aKxuzRagyE5NEGOvrOuAP5RO2ZdNJ/X3/Rh533fK1sOTEEbSsWUvW6iSkZef
|
||||
rsfZBVP32xBhRWkKRdLeLB4W99ADMa0IrTmZPCXHSSE2V4e1o6zWLXcOZeH1Qh8N
|
||||
SkelBweR+8r1Fbvy1r3s7eH7DCbYoGEDVLQGOLvzHKBisQHmoDnnF5E9g1eeNRdg
|
||||
o+vhOKfYCOzeNREJIqS42PHcGhdNRk90ycigPmfUJclz1mDHoMjKR2S5oosTpr65
|
||||
tNPx3CL7GA==
|
||||
-----END CERTIFICATE-----
|
|
@ -1,19 +0,0 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIDBDCCAeygAwIBAgIBAzANBgkqhkiG9w0BAQsFADA8MTowOAYDVQQDDDFNeVNR
|
||||
TF9TZXJ2ZXJfOC4wLjE5X0F1dG9fR2VuZXJhdGVkX0NBX0NlcnRpZmljYXRlMB4X
|
||||
DTIwMDYxMTAzMzg0N1oXDTMwMDYwOTAzMzg0N1owQDE+MDwGA1UEAww1TXlTUUxf
|
||||
U2VydmVyXzguMC4xOV9BdXRvX0dlbmVyYXRlZF9DbGllbnRfQ2VydGlmaWNhdGUw
|
||||
ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDVYSWpOvCTupz82fc85Opv
|
||||
EQ7rkB8X2oOMyBCpkyHKBIr1ZQgRDWBp9UVOASq3GnSElm6+T3Kb1QbOffa8GIlw
|
||||
sjAueKdq5L2eSkmPIEQ7eoO5kEW+4V866hE1LeL/PmHg2lGP0iqZiJYtElhHNQO8
|
||||
3y9I7cm3xWMAA3SSWikVtpJRn3qIp2QSrH+tK+/HHbE5QwtPxdir4ULSCSOaM5Yh
|
||||
Wi5Oto88TZqe1v7SXC864JVvO4LuS7TuSreCdWZyPXTJFBFeCEWSAxonKZrqHbBe
|
||||
CwKML6/0NuzjaQ51c2tzmVI6xpHj3nnu4cSRx6Jf9WBm+35vm0wk4pohX3ptdzeV
|
||||
AgMBAAGjDTALMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADggEBAByQ5zSNeFUH
|
||||
Aw7JlpZHtHaSEeiiyBHke20ziQ07BK1yi/ms2HAWwQkpZv149sjNuIRH8pkTmkZn
|
||||
g8PDzSefjLbC9AsWpWV0XNV22T/cdobqLqMBDDZ2+5bsV+jTrOigWd9/AHVZ93PP
|
||||
IJN8HJn6rtvo2l1bh/CdsX14uVSdofXnuWGabNTydqtMvmCerZsdf6qKqLL+PYwm
|
||||
RDpgWiRUY7KPBSSlKm/9lJzA+bOe4dHeJzxWFVCJcbpoiTFs1je1V8kKQaHtuW39
|
||||
ifX6LTKUMlwEECCbDKM8Yq2tm8NjkjCcnFDtKg8zKGPUu+jrFMN5otiC3wnKcP7r
|
||||
O9EkaPcgYH8=
|
||||
-----END CERTIFICATE-----
|
|
@ -1,27 +0,0 @@
|
|||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEowIBAAKCAQEA1WElqTrwk7qc/Nn3POTqbxEO65AfF9qDjMgQqZMhygSK9WUI
|
||||
EQ1gafVFTgEqtxp0hJZuvk9ym9UGzn32vBiJcLIwLninauS9nkpJjyBEO3qDuZBF
|
||||
vuFfOuoRNS3i/z5h4NpRj9IqmYiWLRJYRzUDvN8vSO3Jt8VjAAN0klopFbaSUZ96
|
||||
iKdkEqx/rSvvxx2xOUMLT8XYq+FC0gkjmjOWIVouTraPPE2antb+0lwvOuCVbzuC
|
||||
7ku07kq3gnVmcj10yRQRXghFkgMaJyma6h2wXgsCjC+v9Dbs42kOdXNrc5lSOsaR
|
||||
49557uHEkceiX/VgZvt+b5tMJOKaIV96bXc3lQIDAQABAoIBAF7yjXmSOn7h6P0y
|
||||
WCuGiTLG2mbDiLJqj2LTm2Z5i+2Cu/qZ7E76Ls63TxF4v3MemH5vGfQhEhR5ZD/6
|
||||
GRJ1sKKvB3WGRqjwA9gtojHH39S/nWGy6vYW/vMOOH37XyjIr3EIdIaUtFQBTSHd
|
||||
Kd71niYrAbVn6fyWHolhADwnVmTMOl5OOAhCdEF4GN3b5aIhIu8BJ7EUzTtHBJIj
|
||||
CAEfjZFjDs1y1cIgGFJkuIQxMfCpq5recU2qwip7YO6fk//WEjOPu7kSf5IEswL8
|
||||
jg1dea9rGBV6KaD2xsgsC6Ll6Sb4BbsrHMfflG3K2Lk3RdVqqTFp1Fn1PTLQE/1S
|
||||
S/SZPYECgYEA9qYcHKHd0+Q5Ty5wgpxKGa4UCWkpwvfvyv4bh8qlmxueB+l2AIdo
|
||||
ZvkM8gTPagPQ3WypAyC2b9iQu70uOJo1NizTtKnpjDdN1YpDjISJuS/P0x73gZwy
|
||||
gmoM5AzMtN4D6IbxXtXnPaYICvwLKU80ouEN5ZPM4/ODLUu6gsp0v2UCgYEA3Xgi
|
||||
zMC4JF0vEKEaK0H6QstaoXUmw/lToZGH3TEojBIkb/2LrHUclygtONh9kJSFb89/
|
||||
jbmRRLAOrx3HZKCNGUmF4H9k5OQyAIv6OGBinvLGqcbqnyNlI+Le8zxySYwKMlEj
|
||||
EMrBCLmSyi0CGFrbZ3mlj/oCET/ql9rNvcK+DHECgYAEx5dH3sMjtgp+RFId1dWB
|
||||
xePRgt4yTwewkVgLO5wV82UOljGZNQaK6Eyd7AXw8f38LHzh+KJQbIvxd2sL4cEi
|
||||
OaAoohpKg0/Y0YMZl//rPMf0OWdmdZZs/I0fZjgZUSwWN3c59T8z7KG/RL8an9RP
|
||||
S7kvN7wCttdV61/D5RR6GQKBgDxCe/WKWpBKaovzydMLWLTj7/0Oi0W3iXHkzzr4
|
||||
LTgvl4qBSofaNbVLUUKuZTv5rXUG2IYPf99YqCYtzBstNDc1MiAriaBeFtzfOW4t
|
||||
i6gEFtoLLbuvPc3N5Sv5vn8Ug5G9UfU3td5R4AbyyCcoUZqOFuZd+EIJSiOXfXOs
|
||||
kVmBAoGBAIU9aPAqhU5LX902oq8KsrpdySONqv5mtoStvl3wo95WIqXNEsFY60wO
|
||||
q02jKQmJJ2MqhkJm2EoF2Mq8+40EZ5sz8LdgeQ/M0yQ9lAhPi4rftwhpe55Ma9dk
|
||||
SE9X1c/DMCBEaIjJqVXdy0/EeArwpb8sHkguVVAZUWxzD+phm1gs
|
||||
-----END RSA PRIVATE KEY-----
|
|
@ -1,46 +0,0 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIDBDCCAeygAwIBAgIBAjANBgkqhkiG9w0BAQsFADA8MTowOAYDVQQDDDFNeVNR
|
||||
TF9TZXJ2ZXJfOC4wLjE5X0F1dG9fR2VuZXJhdGVkX0NBX0NlcnRpZmljYXRlMB4X
|
||||
DTIwMDYxMTAzMzg0NloXDTMwMDYwOTAzMzg0NlowQDE+MDwGA1UEAww1TXlTUUxf
|
||||
U2VydmVyXzguMC4xOV9BdXRvX0dlbmVyYXRlZF9TZXJ2ZXJfQ2VydGlmaWNhdGUw
|
||||
ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCcEnEm5hqP1EbEJycOz8Ua
|
||||
NWp29QdpFUzTWhkKGhVXk+0msmNTw4NBAFB42moY44OU8wvDideOlJNhPRWveD8z
|
||||
G2lxzJA91p0UK4et8ia9MmeuCGhdC9jxJ8X69WNlUiPyy0hI/ZsqRq9Z0C2eW0iL
|
||||
JPXsy4X8Xpw3SFwoXf5pR9RFY5Pb2tuyxqmSestu2VXT/NQjJg4CVDR3mFcHPXZB
|
||||
4elRzH0WshExEGkgy0bg20MJeRc2Qdb5Xx+EakbmwroDWaCn3NSGqQ7jv6Vw0doy
|
||||
TGvS6h6RHBxnyqRfRgKGlCoOMG9/5+rFJC00QpCUG2vHXHWGoWlMlJ3foN7rj5v9
|
||||
AgMBAAGjDTALMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADggEBAJ5zt2rj4Ag6
|
||||
zpN59AWC1Fur8g8l41ksHkSpKPp+PtyO/ngvbMqBpfmK1e7JCKZv/68QXfMyWWAI
|
||||
hwalqZkXXWHKjuz3wE7dE25PXFXtGJtcZAaj10xt98fzdqt8lQSwh2kbfNwZIz1F
|
||||
sgAStgE7+ZTcqTgvNB76Os1UK0to+/P0VBWktaVFdyub4Nc2SdPVnZNvrRBXBwOD
|
||||
3V8ViwywDOFoE7DvCvwx/SVsvoC0Z4j3AMMovO6oHicP7uU83qsQgm1Qru3YeoLR
|
||||
+DoVi7IPHbWvN7MqFYn3YjNlByO2geblY7MR0BlqbFlmFrqLsUfjsh2ys7/U/knC
|
||||
dN/klu446fI=
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEowIBAAKCAQEAnBJxJuYaj9RGxCcnDs/FGjVqdvUHaRVM01oZChoVV5PtJrJj
|
||||
U8ODQQBQeNpqGOODlPMLw4nXjpSTYT0Vr3g/MxtpccyQPdadFCuHrfImvTJnrgho
|
||||
XQvY8SfF+vVjZVIj8stISP2bKkavWdAtnltIiyT17MuF/F6cN0hcKF3+aUfURWOT
|
||||
29rbssapknrLbtlV0/zUIyYOAlQ0d5hXBz12QeHpUcx9FrIRMRBpIMtG4NtDCXkX
|
||||
NkHW+V8fhGpG5sK6A1mgp9zUhqkO47+lcNHaMkxr0uoekRwcZ8qkX0YChpQqDjBv
|
||||
f+fqxSQtNEKQlBtrx1x1hqFpTJSd36De64+b/QIDAQABAoIBAFiah66Dt9SruLkn
|
||||
WR8piUaFyLlcBib8Nq9OWSTJBhDAJERxxb4KIvvGB+l0ZgNXNp5bFPSfzsZdRwZP
|
||||
PX5uj8Kd71Dxx3mz211WESMJdEC42u+MSmN4lGLkJ5t/sDwXU91E1vbJM0ve8THV
|
||||
4/Ag9qA4DX2vVZOeyqT/6YHpSsPNZplqzrbAiwrfHwkctHfgqwOf3QLfhmVQgfCS
|
||||
VwidBldEUv2whSIiIxh4Rv5St4kA68IBCbJxdpOpyuQBkk6CkxZ7VN9FqOuSd4Pk
|
||||
Wm7iWyBMZsCmELZh5XAXld4BEt87C5R4CvbPBDZxAv3THk1DNNvpy3PFQfwARRFb
|
||||
SAToYMECgYEAyL7U8yxpzHDYWd3oCx6vTi9p9N/z0FfAkWrRF6dm4UcSklNiT1Aq
|
||||
EOnTA+SaW8tV3E64gCWcY23gNP8so/ZseWj6L+peHwtchaP9+KB7yGw2A+05+lOx
|
||||
VetLTjAOmfpiUXFe5w1q4C1RGhLjZjjzW+GvwdAuchQgUEFaomrV+PUCgYEAxwfH
|
||||
cmVGFbAktcjU4HSRjKSfawCrut+3YUOLybyku3Q/hP9amG8qkVTFe95CTLjLe2D0
|
||||
ccaTTpofFEJ32COeck0g0Ujn/qQ+KXRoauOYs4FB1DtqMpqB78wufWEUpDpbd9/h
|
||||
J+gJdC/IADd4tJW9zA92g8IA7ZtFmqDtiSpQ0ekCgYAQGkaorvJZpN+l7cf0RGTZ
|
||||
h7IfI2vCVZer0n6tQA9fmLzjoe6r4AlPzAHSOR8sp9XeUy43kUzHKQQoHCPvjw/K
|
||||
eWJAP7OHF/k2+x2fOPhU7mEy1W+mJdp+wt4Kio5RSaVjVQ3AyPG+w8PSrJszEvRq
|
||||
dWMMz+851WV2KpfjmWBKlQKBgQC++4j4DZQV5aMkSKV1CIZOBf3vaIJhXKEUFQPD
|
||||
PmB4fBEjpwCg+zNGp6iktt65zi17o8qMjrb1mtCt2SY04eD932LZUHNFlwcLMmes
|
||||
Ad+aiDLJ24WJL1f16eDGcOyktlblDZB5gZ/ovJzXEGOkLXglosTfo77OQculmDy2
|
||||
/UL2WQKBgGeKasmGNfiYAcWio+KXgFkHXWtAXB9B91B1OFnCa40wx+qnl71MIWQH
|
||||
PQ/CZFNWOfGiNEJIZjrHsfNJoeXkhq48oKcT0AVCDYyLV0VxDO4ejT95mGW6njNd
|
||||
JpvmhwwAjOvuWVr0tn4iXlSK8irjlJHmwcRjLTJq97vE9fsA2MjI
|
||||
-----END RSA PRIVATE KEY-----
|
|
@ -1,27 +0,0 @@
|
|||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEpAIBAAKCAQEA1zVmMhPqpSPMmYkKh5wwlRD5XuS8YWJKEM6tjFx61VK8qxHE
|
||||
YngkC2KnL5EuKAjQZIF3tJskwt0hAat047CCCZxrkNEpbVvSnvnk+A/8bg/Ww1n3
|
||||
qxzfifhsWfpUKlDnwrtH+ftt+5rZeEkf37XAPy7ZjzecAF9SDV6WSiPeAxUX2+hN
|
||||
dId42Pf45woo4LFGUlQeagCFkD/R0dpNIMGwcnkKCUikiBqr2ijSIgvRtBfZ9fBG
|
||||
jFGER2uE/Eay4AgcQsHue8skRwDCng8OnqtPnBtTytmqTy9V/BRgsVKUoksm6wsx
|
||||
kUYwgHeaq7UCvlCm25SZ7yRyd4k8t0BKDf2h+wIDAQABAoIBAEQcrHmRACTADdNS
|
||||
IjkFYALt2l8EOfMAbryfDSJtapr1kqz59JPNvmq0EIHnixo0n/APYdmReLML1ZR3
|
||||
tYkSpjVwgkLVUC1CcIjMQoGYXaZf8PLnGJHZk45RR8m6hsTV0mQ5bfBaeVa2jbma
|
||||
OzJMjcnxg/3l9cPQZ2G/3AUfEPccMxOXp1KRz3mUQcGnKJGtDbN/kfmntcwYoxaE
|
||||
Zg4RoeKAoMpK1SSHAiJKe7TnztINJ7uygR9XSzNd6auY8A3vomSIjpYO7XL+lh7L
|
||||
izm4Ir3Gb/eCYBvWgQyQa2KCJgK/sQyEs3a09ngofSEUhQJQYhgZDwUj+fDDOGqj
|
||||
hCZOA8ECgYEA+ZWuHdcUQ3ygYhLds2QcogUlIsx7C8n/Gk/FUrqqXJrTkuO0Eqqa
|
||||
B47lCITvmn2zm0ODfSFIARgKEUEDLS/biZYv7SUTrFqBLcet+aGI7Dpv91CgB75R
|
||||
tNzcIf8VxoiP0jPqdbh9mLbbxGi5Uc4p9TVXRljC4hkswaouebWee0sCgYEA3L2E
|
||||
YB3kiHrhPI9LHS5Px9C1w+NOu5wP5snxrDGEgaFCvL6zgY6PflacppgnmTXl8D1x
|
||||
im0IDKSw5dP3FFonSVXReq3CXDql7UnhfTCiLDahV7bLxTH42FofcBpDN3ERdOal
|
||||
58RwQh6VrLkzQRVoObo+hbGlFiwwSAfQC509FhECgYBsRSBpVXo25IN2yBRg09cP
|
||||
+gdoFyhxrsj5kw1YnB13WrrZh+oABv4WtUhp77E5ZbpaamlKCPwBbXpAjeFg4tfr
|
||||
0bksuN7V79UGFQ9FsWuCfr8/nDwv38H2IbFlFhFONMOfPmJBey0Q6JJhm8R41mSh
|
||||
OOiJXcv85UrjIH5U0hLUDQKBgQDVLOU5WcUJlPoOXSgiT0ZW5xWSzuOLRUUKEf6l
|
||||
19BqzAzCcLy0orOrRAPW01xylt2v6/bJw1Ahva7k1ZZo/kOwjANYoZPxM+ZoSZBN
|
||||
MXl8j2mzZuJVV1RFxItV3NcLJNPB/Lk+IbRz9kt/2f9InF7iWR3mSU/wIM6j0X+2
|
||||
p6yFsQKBgQCM/ldWb511lA+SNkqXB2P6WXAgAM/7+jwsNHX2ia2Ikufm4SUEKMSv
|
||||
mti/nZkHDHsrHU4wb/2cOAywMELzv9EHzdcoenjBQP65OAc/1qWJs+LnBcCXfqKk
|
||||
aHjEZW6+brkHdRGLLY3YAHlt/AUL+RsKPJfN72i/FSpmu+52G36eeQ==
|
||||
-----END RSA PRIVATE KEY-----
|
|
@ -1,9 +0,0 @@
|
|||
-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1zVmMhPqpSPMmYkKh5ww
|
||||
lRD5XuS8YWJKEM6tjFx61VK8qxHEYngkC2KnL5EuKAjQZIF3tJskwt0hAat047CC
|
||||
CZxrkNEpbVvSnvnk+A/8bg/Ww1n3qxzfifhsWfpUKlDnwrtH+ftt+5rZeEkf37XA
|
||||
Py7ZjzecAF9SDV6WSiPeAxUX2+hNdId42Pf45woo4LFGUlQeagCFkD/R0dpNIMGw
|
||||
cnkKCUikiBqr2ijSIgvRtBfZ9fBGjFGER2uE/Eay4AgcQsHue8skRwDCng8OnqtP
|
||||
nBtTytmqTy9V/BRgsVKUoksm6wsxkUYwgHeaq7UCvlCm25SZ7yRyd4k8t0BKDf2h
|
||||
+wIDAQAB
|
||||
-----END PUBLIC KEY-----
|
|
@ -1,19 +0,0 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIDBDCCAeygAwIBAgIBAjANBgkqhkiG9w0BAQsFADA8MTowOAYDVQQDDDFNeVNR
|
||||
TF9TZXJ2ZXJfOC4wLjE5X0F1dG9fR2VuZXJhdGVkX0NBX0NlcnRpZmljYXRlMB4X
|
||||
DTIwMDYxMTAzMzg0NloXDTMwMDYwOTAzMzg0NlowQDE+MDwGA1UEAww1TXlTUUxf
|
||||
U2VydmVyXzguMC4xOV9BdXRvX0dlbmVyYXRlZF9TZXJ2ZXJfQ2VydGlmaWNhdGUw
|
||||
ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCcEnEm5hqP1EbEJycOz8Ua
|
||||
NWp29QdpFUzTWhkKGhVXk+0msmNTw4NBAFB42moY44OU8wvDideOlJNhPRWveD8z
|
||||
G2lxzJA91p0UK4et8ia9MmeuCGhdC9jxJ8X69WNlUiPyy0hI/ZsqRq9Z0C2eW0iL
|
||||
JPXsy4X8Xpw3SFwoXf5pR9RFY5Pb2tuyxqmSestu2VXT/NQjJg4CVDR3mFcHPXZB
|
||||
4elRzH0WshExEGkgy0bg20MJeRc2Qdb5Xx+EakbmwroDWaCn3NSGqQ7jv6Vw0doy
|
||||
TGvS6h6RHBxnyqRfRgKGlCoOMG9/5+rFJC00QpCUG2vHXHWGoWlMlJ3foN7rj5v9
|
||||
AgMBAAGjDTALMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADggEBAJ5zt2rj4Ag6
|
||||
zpN59AWC1Fur8g8l41ksHkSpKPp+PtyO/ngvbMqBpfmK1e7JCKZv/68QXfMyWWAI
|
||||
hwalqZkXXWHKjuz3wE7dE25PXFXtGJtcZAaj10xt98fzdqt8lQSwh2kbfNwZIz1F
|
||||
sgAStgE7+ZTcqTgvNB76Os1UK0to+/P0VBWktaVFdyub4Nc2SdPVnZNvrRBXBwOD
|
||||
3V8ViwywDOFoE7DvCvwx/SVsvoC0Z4j3AMMovO6oHicP7uU83qsQgm1Qru3YeoLR
|
||||
+DoVi7IPHbWvN7MqFYn3YjNlByO2geblY7MR0BlqbFlmFrqLsUfjsh2ys7/U/knC
|
||||
dN/klu446fI=
|
||||
-----END CERTIFICATE-----
|
|
@ -1,27 +0,0 @@
|
|||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEowIBAAKCAQEAnBJxJuYaj9RGxCcnDs/FGjVqdvUHaRVM01oZChoVV5PtJrJj
|
||||
U8ODQQBQeNpqGOODlPMLw4nXjpSTYT0Vr3g/MxtpccyQPdadFCuHrfImvTJnrgho
|
||||
XQvY8SfF+vVjZVIj8stISP2bKkavWdAtnltIiyT17MuF/F6cN0hcKF3+aUfURWOT
|
||||
29rbssapknrLbtlV0/zUIyYOAlQ0d5hXBz12QeHpUcx9FrIRMRBpIMtG4NtDCXkX
|
||||
NkHW+V8fhGpG5sK6A1mgp9zUhqkO47+lcNHaMkxr0uoekRwcZ8qkX0YChpQqDjBv
|
||||
f+fqxSQtNEKQlBtrx1x1hqFpTJSd36De64+b/QIDAQABAoIBAFiah66Dt9SruLkn
|
||||
WR8piUaFyLlcBib8Nq9OWSTJBhDAJERxxb4KIvvGB+l0ZgNXNp5bFPSfzsZdRwZP
|
||||
PX5uj8Kd71Dxx3mz211WESMJdEC42u+MSmN4lGLkJ5t/sDwXU91E1vbJM0ve8THV
|
||||
4/Ag9qA4DX2vVZOeyqT/6YHpSsPNZplqzrbAiwrfHwkctHfgqwOf3QLfhmVQgfCS
|
||||
VwidBldEUv2whSIiIxh4Rv5St4kA68IBCbJxdpOpyuQBkk6CkxZ7VN9FqOuSd4Pk
|
||||
Wm7iWyBMZsCmELZh5XAXld4BEt87C5R4CvbPBDZxAv3THk1DNNvpy3PFQfwARRFb
|
||||
SAToYMECgYEAyL7U8yxpzHDYWd3oCx6vTi9p9N/z0FfAkWrRF6dm4UcSklNiT1Aq
|
||||
EOnTA+SaW8tV3E64gCWcY23gNP8so/ZseWj6L+peHwtchaP9+KB7yGw2A+05+lOx
|
||||
VetLTjAOmfpiUXFe5w1q4C1RGhLjZjjzW+GvwdAuchQgUEFaomrV+PUCgYEAxwfH
|
||||
cmVGFbAktcjU4HSRjKSfawCrut+3YUOLybyku3Q/hP9amG8qkVTFe95CTLjLe2D0
|
||||
ccaTTpofFEJ32COeck0g0Ujn/qQ+KXRoauOYs4FB1DtqMpqB78wufWEUpDpbd9/h
|
||||
J+gJdC/IADd4tJW9zA92g8IA7ZtFmqDtiSpQ0ekCgYAQGkaorvJZpN+l7cf0RGTZ
|
||||
h7IfI2vCVZer0n6tQA9fmLzjoe6r4AlPzAHSOR8sp9XeUy43kUzHKQQoHCPvjw/K
|
||||
eWJAP7OHF/k2+x2fOPhU7mEy1W+mJdp+wt4Kio5RSaVjVQ3AyPG+w8PSrJszEvRq
|
||||
dWMMz+851WV2KpfjmWBKlQKBgQC++4j4DZQV5aMkSKV1CIZOBf3vaIJhXKEUFQPD
|
||||
PmB4fBEjpwCg+zNGp6iktt65zi17o8qMjrb1mtCt2SY04eD932LZUHNFlwcLMmes
|
||||
Ad+aiDLJ24WJL1f16eDGcOyktlblDZB5gZ/ovJzXEGOkLXglosTfo77OQculmDy2
|
||||
/UL2WQKBgGeKasmGNfiYAcWio+KXgFkHXWtAXB9B91B1OFnCa40wx+qnl71MIWQH
|
||||
PQ/CZFNWOfGiNEJIZjrHsfNJoeXkhq48oKcT0AVCDYyLV0VxDO4ejT95mGW6njNd
|
||||
JpvmhwwAjOvuWVr0tn4iXlSK8irjlJHmwcRjLTJq97vE9fsA2MjI
|
||||
-----END RSA PRIVATE KEY-----
|
|
@ -1,31 +0,0 @@
|
|||
.eunit
|
||||
deps
|
||||
*.so
|
||||
.iml
|
||||
.idea
|
||||
*.o
|
||||
*.beam
|
||||
*.plt
|
||||
erl_crash.dump
|
||||
ebin
|
||||
rel/example_project
|
||||
.concrete/DEV_MODE
|
||||
.rebar
|
||||
.erlang.mk/
|
||||
emqx_auth_mysql.d
|
||||
ct.coverdata
|
||||
logs/
|
||||
test/ct.cover.spec
|
||||
test/*.beam
|
||||
cover/
|
||||
eunit.coverdata
|
||||
data
|
||||
.placeholder
|
||||
_build/
|
||||
rebar.lock
|
||||
erlang.mk
|
||||
rebar3.crashdump
|
||||
etc/emqx_auth_mysql.conf.rendered
|
||||
.rebar3/
|
||||
*.swp
|
||||
.DS_Store
|
|
@ -1,167 +0,0 @@
|
|||
emqx_auth_mysql
|
||||
===============
|
||||
|
||||
Authentication, ACL with MySQL Database.
|
||||
|
||||
Notice: changed mysql driver to [mysql-otp](https://github.com/mysql-otp/mysql-otp).
|
||||
|
||||
Features
|
||||
---------
|
||||
|
||||
- Full *Authentication*, *Superuser*, *ACL* support
|
||||
- IPv4, IPv6 and TLS support
|
||||
- Connection pool by [ecpool](https://github.com/emqx/ecpool)
|
||||
- Completely cover MySQL 5.7, MySQL 8 in our tests
|
||||
|
||||
Build Plugin
|
||||
-------------
|
||||
|
||||
make && make tests
|
||||
|
||||
Configure Plugin
|
||||
----------------
|
||||
|
||||
File: etc/emqx_auth_mysql.conf
|
||||
|
||||
```
|
||||
## MySQL server address.
|
||||
##
|
||||
## Value: Port | IP:Port
|
||||
##
|
||||
## Examples: 3306, 127.0.0.1:3306, localhost:3306
|
||||
auth.mysql.server = 127.0.0.1:3306
|
||||
|
||||
## MySQL pool size.
|
||||
##
|
||||
## Value: Number
|
||||
auth.mysql.pool = 8
|
||||
|
||||
## MySQL username.
|
||||
##
|
||||
## Value: String
|
||||
## auth.mysql.username =
|
||||
|
||||
## MySQL Password.
|
||||
##
|
||||
## Value: String
|
||||
## auth.mysql.password =
|
||||
|
||||
## MySQL database.
|
||||
##
|
||||
## Value: String
|
||||
auth.mysql.database = mqtt
|
||||
|
||||
## Variables: %u = username, %c = clientid
|
||||
|
||||
## Authentication query.
|
||||
##
|
||||
## Note that column names should be 'password' and 'salt' (if used).
|
||||
## In case column names differ in your DB - please use aliases,
|
||||
## e.g. "my_column_name as password".
|
||||
##
|
||||
## Value: SQL
|
||||
##
|
||||
## Variables:
|
||||
## - %u: username
|
||||
## - %c: clientid
|
||||
## - %C: common name of client TLS cert
|
||||
## - %d: subject of client TLS cert
|
||||
##
|
||||
auth.mysql.auth_query = select password from mqtt_user where username = '%u' limit 1
|
||||
## auth.mysql.auth_query = select password_hash as password from mqtt_user where username = '%u' limit 1
|
||||
|
||||
## Password hash.
|
||||
##
|
||||
## Value: plain | md5 | sha | sha256 | bcrypt
|
||||
auth.mysql.password_hash = sha256
|
||||
|
||||
## sha256 with salt prefix
|
||||
## auth.mysql.password_hash = salt,sha256
|
||||
|
||||
## bcrypt with salt only prefix
|
||||
## auth.mysql.password_hash = salt,bcrypt
|
||||
|
||||
## sha256 with salt suffix
|
||||
## auth.mysql.password_hash = sha256,salt
|
||||
|
||||
## pbkdf2 with macfun iterations dklen
|
||||
## macfun: md4, md5, ripemd160, sha, sha224, sha256, sha384, sha512
|
||||
## auth.mysql.password_hash = pbkdf2,sha256,1000,20
|
||||
|
||||
## Superuser query.
|
||||
##
|
||||
## Value: SQL
|
||||
##
|
||||
## Variables:
|
||||
## - %u: username
|
||||
## - %c: clientid
|
||||
## - %C: common name of client TLS cert
|
||||
## - %d: subject of client TLS cert
|
||||
auth.mysql.super_query = select is_superuser from mqtt_user where username = '%u' limit 1
|
||||
|
||||
## ACL query.
|
||||
##
|
||||
## Value: SQL
|
||||
##
|
||||
## Variables:
|
||||
## - %a: ipaddr
|
||||
## - %u: username
|
||||
## - %c: clientid
|
||||
## Note: You can add the 'ORDER BY' statement to control the rules match order
|
||||
auth.mysql.acl_query = select allow, ipaddr, username, clientid, access, topic from mqtt_acl where ipaddr = '%a' or username = '%u' or username = '$all' or clientid = '%c'
|
||||
|
||||
```
|
||||
|
||||
Import mqtt.sql
|
||||
---------------
|
||||
|
||||
Import mqtt.sql into your database.
|
||||
|
||||
Load Plugin
|
||||
-----------
|
||||
|
||||
./bin/emqx_ctl plugins load emqx_auth_mysql
|
||||
|
||||
Auth Table
|
||||
----------
|
||||
|
||||
Notice: This is a demo table. You could authenticate with any user table.
|
||||
|
||||
```sql
|
||||
CREATE TABLE `mqtt_user` (
|
||||
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`username` varchar(100) DEFAULT NULL,
|
||||
`password` varchar(100) DEFAULT NULL,
|
||||
`salt` varchar(35) DEFAULT NULL,
|
||||
`is_superuser` tinyint(1) DEFAULT 0,
|
||||
`created` datetime DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `mqtt_username` (`username`)
|
||||
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
|
||||
```
|
||||
|
||||
ACL Table
|
||||
----------
|
||||
|
||||
```sql
|
||||
CREATE TABLE `mqtt_acl` (
|
||||
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`allow` int(1) DEFAULT NULL COMMENT '0: deny, 1: allow',
|
||||
`ipaddr` varchar(60) DEFAULT NULL COMMENT 'IpAddress',
|
||||
`username` varchar(100) DEFAULT NULL COMMENT 'Username',
|
||||
`clientid` varchar(100) DEFAULT NULL COMMENT 'ClientId',
|
||||
`access` int(2) NOT NULL COMMENT '1: subscribe, 2: publish, 3: pubsub',
|
||||
`topic` varchar(100) NOT NULL DEFAULT '' COMMENT 'Topic Filter',
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
```
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
Apache License Version 2.0
|
||||
|
||||
Author
|
||||
------
|
||||
|
||||
EMQ X Team.
|
|
@ -1,131 +0,0 @@
|
|||
##--------------------------------------------------------------------
|
||||
## MySQL Auth/ACL Plugin
|
||||
##--------------------------------------------------------------------
|
||||
|
||||
## MySQL server address.
|
||||
##
|
||||
## Value: Port | IP:Port
|
||||
##
|
||||
## Examples: 3306, 127.0.0.1:3306, localhost:3306
|
||||
auth.mysql.server = "127.0.0.1:3306"
|
||||
|
||||
## MySQL pool size.
|
||||
##
|
||||
## Value: Number
|
||||
auth.mysql.pool = 8
|
||||
|
||||
## MySQL username.
|
||||
##
|
||||
## Value: String
|
||||
#auth.mysql.username =
|
||||
|
||||
## MySQL password.
|
||||
##
|
||||
## Value: String
|
||||
#auth.mysql.password =
|
||||
|
||||
## MySQL database.
|
||||
##
|
||||
## Value: String
|
||||
auth.mysql.database = mqtt
|
||||
|
||||
## MySQL query timeout
|
||||
##
|
||||
## Value: Duration
|
||||
## auth.mysql.query_timeout = 5s
|
||||
|
||||
## Variables: %u = username, %c = clientid
|
||||
|
||||
## Authentication query.
|
||||
##
|
||||
## Note that column names should be 'password' and 'salt' (if used).
|
||||
## In case column names differ in your DB - please use aliases,
|
||||
## e.g. "my_column_name as password".
|
||||
##
|
||||
## Value: SQL
|
||||
##
|
||||
## Variables:
|
||||
## - %u: username
|
||||
## - %c: clientid
|
||||
## - %C: common name of client TLS cert
|
||||
## - %d: subject of client TLS cert
|
||||
##
|
||||
auth.mysql.auth_query = "select password from mqtt_user where username = '%u' limit 1"
|
||||
## auth.mysql.auth_query = select password_hash as password from mqtt_user where username = '%u' limit 1
|
||||
|
||||
## Password hash.
|
||||
##
|
||||
## Value: plain | md5 | sha | sha256 | bcrypt
|
||||
auth.mysql.password_hash = sha256
|
||||
|
||||
## sha256 with salt prefix
|
||||
## auth.mysql.password_hash = "salt,sha256"
|
||||
|
||||
## bcrypt with salt only prefix
|
||||
## auth.mysql.password_hash = "salt,bcrypt"
|
||||
|
||||
## sha256 with salt suffix
|
||||
## auth.mysql.password_hash = "sha256,salt"
|
||||
|
||||
## pbkdf2 with macfun iterations dklen
|
||||
## macfun: md4, md5, ripemd160, sha, sha224, sha256, sha384, sha512
|
||||
## auth.mysql.password_hash = "pbkdf2,sha256,1000,20"
|
||||
|
||||
## Superuser query.
|
||||
##
|
||||
## Value: SQL
|
||||
##
|
||||
## Variables:
|
||||
## - %u: username
|
||||
## - %c: clientid
|
||||
## - %C: common name of client TLS cert
|
||||
## - %d: subject of client TLS cert
|
||||
##
|
||||
auth.mysql.super_query = "select is_superuser from mqtt_user where username = '%u' limit 1"
|
||||
|
||||
## ACL query.
|
||||
##
|
||||
## Value: SQL
|
||||
##
|
||||
## Variables:
|
||||
## - %a: ipaddr
|
||||
## - %u: username
|
||||
## - %c: clientid
|
||||
##
|
||||
## Note: You can add the 'ORDER BY' statement to control the rules match order
|
||||
auth.mysql.acl_query = "select allow, ipaddr, username, clientid, access, topic from mqtt_acl where ipaddr = '%a' or username = '%u' or username = '$all' or clientid = '%c'"
|
||||
|
||||
## Mysql ssl configuration.
|
||||
##
|
||||
## Value: on | off
|
||||
## auth.mysql.ssl.enable = off
|
||||
|
||||
## CA certificate.
|
||||
##
|
||||
## Value: File
|
||||
#auth.mysql.ssl.cacertfile = /path/to/ca.pem
|
||||
|
||||
## Client ssl certificate.
|
||||
##
|
||||
## Value: File
|
||||
#auth.mysql.ssl.certfile = /path/to/your/clientcert.pem
|
||||
|
||||
## Client ssl keyfile.
|
||||
##
|
||||
## Value: File
|
||||
#auth.mysql.ssl.keyfile = /path/to/your/clientkey.pem
|
||||
|
||||
## In mode verify_none the default behavior is to allow all x509-path
|
||||
## validation errors.
|
||||
##
|
||||
## Value: true | false
|
||||
#auth.mysql.ssl.verify = false
|
||||
|
||||
## If not specified, the server's names returned in server's certificate is validated against
|
||||
## what's provided `auth.mysql.server` config's host part.
|
||||
## Setting to 'disable' will make EMQ X ignore unmatched server names.
|
||||
## If set with a host name, the server's names returned in server's certificate is validated
|
||||
## against this value.
|
||||
##
|
||||
## Value: String | disable
|
||||
## auth.mysql.ssl.server_name_indication = disable
|
|
@ -1,23 +0,0 @@
|
|||
|
||||
-define(APP, emqx_auth_mysql).
|
||||
|
||||
-record(auth_metrics, {
|
||||
success = 'client.auth.success',
|
||||
failure = 'client.auth.failure',
|
||||
ignore = 'client.auth.ignore'
|
||||
}).
|
||||
|
||||
-record(acl_metrics, {
|
||||
allow = 'client.acl.allow',
|
||||
deny = 'client.acl.deny',
|
||||
ignore = 'client.acl.ignore'
|
||||
}).
|
||||
|
||||
-define(METRICS(Type), tl(tuple_to_list(#Type{}))).
|
||||
-define(METRICS(Type, K), #Type{}#Type.K).
|
||||
|
||||
-define(AUTH_METRICS, ?METRICS(auth_metrics)).
|
||||
-define(AUTH_METRICS(K), ?METRICS(auth_metrics, K)).
|
||||
|
||||
-define(ACL_METRICS, ?METRICS(acl_metrics)).
|
||||
-define(ACL_METRICS(K), ?METRICS(acl_metrics, K)).
|
|
@ -1,41 +0,0 @@
|
|||
|
||||
DROP TABLE IF EXISTS `mqtt_acl`;
|
||||
|
||||
CREATE TABLE `mqtt_acl` (
|
||||
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`allow` int(1) DEFAULT NULL COMMENT '0: deny, 1: allow',
|
||||
`ipaddr` varchar(60) DEFAULT NULL COMMENT 'IpAddress',
|
||||
`username` varchar(100) DEFAULT NULL COMMENT 'Username',
|
||||
`clientid` varchar(100) DEFAULT NULL COMMENT 'ClientId',
|
||||
`access` int(2) NOT NULL COMMENT '1: subscribe, 2: publish, 3: pubsub',
|
||||
`topic` varchar(100) NOT NULL DEFAULT '' COMMENT 'Topic Filter',
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
LOCK TABLES `mqtt_acl` WRITE;
|
||||
|
||||
INSERT INTO `mqtt_acl` (`id`, `allow`, `ipaddr`, `username`, `clientid`, `access`, `topic`)
|
||||
VALUES
|
||||
(1,1,NULL,'$all',NULL,2,'#'),
|
||||
(2,0,NULL,'$all',NULL,1,'$SYS/#'),
|
||||
(3,0,NULL,'$all',NULL,1,'eq #'),
|
||||
(4,1,'127.0.0.1',NULL,NULL,2,'$SYS/#'),
|
||||
(5,1,'127.0.0.1',NULL,NULL,2,'#'),
|
||||
(6,1,NULL,'dashboard',NULL,1,'$SYS/#');
|
||||
|
||||
UNLOCK TABLES;
|
||||
|
||||
|
||||
DROP TABLE IF EXISTS `mqtt_user`;
|
||||
|
||||
CREATE TABLE `mqtt_user` (
|
||||
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`username` varchar(100) DEFAULT NULL,
|
||||
`password` varchar(100) DEFAULT NULL,
|
||||
`salt` varchar(35) DEFAULT NULL,
|
||||
`is_superuser` tinyint(1) DEFAULT 0,
|
||||
`created` datetime DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `mqtt_username` (`username`)
|
||||
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4;
|
||||
|
|
@ -1,156 +0,0 @@
|
|||
%%-*- mode: erlang -*-
|
||||
%% emqx_auth_mysql config mapping
|
||||
{mapping, "auth.mysql.server", "emqx_auth_mysql.server", [
|
||||
{default, {"127.0.0.1", 3306}},
|
||||
{datatype, [integer, ip, string]}
|
||||
]}.
|
||||
|
||||
{mapping, "auth.mysql.pool", "emqx_auth_mysql.server", [
|
||||
{default, 8},
|
||||
{datatype, integer}
|
||||
]}.
|
||||
|
||||
{mapping, "auth.mysql.username", "emqx_auth_mysql.server", [
|
||||
{default, ""},
|
||||
{datatype, string}
|
||||
]}.
|
||||
|
||||
{mapping, "auth.mysql.password", "emqx_auth_mysql.server", [
|
||||
{default, ""},
|
||||
{datatype, string}
|
||||
]}.
|
||||
|
||||
{mapping, "auth.mysql.database", "emqx_auth_mysql.server", [
|
||||
{default, "mqtt"},
|
||||
{datatype, string}
|
||||
]}.
|
||||
|
||||
{mapping, "auth.mysql.query_timeout", "emqx_auth_mysql.server", [
|
||||
{default, ""},
|
||||
{datatype, string}
|
||||
]}.
|
||||
|
||||
{mapping, "auth.mysql.ssl.enable", "emqx_auth_mysql.server", [
|
||||
{default, off},
|
||||
{datatype, flag}
|
||||
]}.
|
||||
|
||||
{mapping, "auth.mysql.ssl.cafile", "emqx_auth_mysql.server", [
|
||||
{datatype, string}
|
||||
]}.
|
||||
|
||||
{mapping, "auth.mysql.ssl.cacertfile", "emqx_auth_mysql.server", [
|
||||
{datatype, string}
|
||||
]}.
|
||||
|
||||
%% FIXME: compatible with 4.0-4.2 version format, plan to delete in 5.0
|
||||
{mapping, "auth.mysql.ssl.certfile", "emqx_auth_mysql.server", [
|
||||
{datatype, string}
|
||||
]}.
|
||||
|
||||
{mapping, "auth.mysql.ssl.keyfile", "emqx_auth_mysql.server", [
|
||||
{datatype, string}
|
||||
]}.
|
||||
|
||||
{mapping, "auth.mysql.ssl.verify", "emqx_auth_mysql.server", [
|
||||
{default, false},
|
||||
{datatype, {enum, [true, false]}}
|
||||
]}.
|
||||
|
||||
{mapping, "auth.mysql.ssl.server_name_indication", "emqx_auth_mysql.server", [
|
||||
{datatype, string}
|
||||
]}.
|
||||
|
||||
{translation, "emqx_auth_mysql.server", fun(Conf) ->
|
||||
{MyHost, MyPort} =
|
||||
case cuttlefish:conf_get("auth.mysql.server", Conf) of
|
||||
{Ip, Port} -> {Ip, Port};
|
||||
S -> case string:tokens(S, ":") of
|
||||
[Domain] -> {Domain, 3306};
|
||||
[Domain, Port] -> {Domain, list_to_integer(Port)}
|
||||
end
|
||||
end,
|
||||
Pool = cuttlefish:conf_get("auth.mysql.pool", Conf),
|
||||
Username = cuttlefish:conf_get("auth.mysql.username", Conf),
|
||||
Passwd = cuttlefish:conf_get("auth.mysql.password", Conf),
|
||||
DB = cuttlefish:conf_get("auth.mysql.database", Conf),
|
||||
Timeout = case cuttlefish:conf_get("auth.mysql.query_timeout", Conf) of
|
||||
"" -> 300000;
|
||||
Duration ->
|
||||
case cuttlefish_duration:parse(Duration, ms) of
|
||||
{error, Reason} -> error(Reason);
|
||||
Ms when is_integer(Ms) -> Ms
|
||||
end
|
||||
end,
|
||||
Options = [{pool_size, Pool},
|
||||
{auto_reconnect, 1},
|
||||
{host, MyHost},
|
||||
{port, MyPort},
|
||||
{user, Username},
|
||||
{password, Passwd},
|
||||
{database, DB},
|
||||
{encoding, utf8},
|
||||
{query_timeout, Timeout},
|
||||
{keep_alive, true}],
|
||||
Filter = fun(Opts) -> [{K, V} || {K, V} <- Opts, V =/= undefined] end,
|
||||
Options1 =
|
||||
case cuttlefish:conf_get("auth.mysql.ssl.enable", Conf) of
|
||||
true ->
|
||||
%% FIXME: compatible with 4.0-4.2 version format, plan to delete in 5.0
|
||||
CA = cuttlefish:conf_get(
|
||||
"auth.mysql.ssl.cacertfile", Conf,
|
||||
cuttlefish:conf_get("auth.mysql.ssl.cafile", Conf, undefined)
|
||||
),
|
||||
Cert = cuttlefish:conf_get("auth.mysql.ssl.certfile", Conf, undefined),
|
||||
Key = cuttlefish:conf_get("auth.mysql.ssl.keyfile", Conf, undefined),
|
||||
Verify = case cuttlefish:conf_get("auth.mysql.ssl.verify", Conf, false) of
|
||||
true -> verify_peer;
|
||||
false -> verify_none
|
||||
end,
|
||||
SNI = case cuttlefish:conf_get("auth.mysql.ssl.server_name_indication", Conf, undefined) of
|
||||
"disable" -> disable;
|
||||
SNI0 -> SNI0
|
||||
end,
|
||||
Options ++ [{ssl, Filter([{server_name_indication, SNI},
|
||||
{cacertfile, CA},
|
||||
{certfile, Cert},
|
||||
{keyfile, Key},
|
||||
{verify, Verify}
|
||||
])
|
||||
}];
|
||||
_ ->
|
||||
Options
|
||||
end,
|
||||
case inet:parse_address(MyHost) of
|
||||
{ok, IpAddr} when tuple_size(IpAddr) =:= 8 ->
|
||||
[{tcp_options, [inet6]} | Options1];
|
||||
_ ->
|
||||
Options1
|
||||
end
|
||||
end}.
|
||||
|
||||
{mapping, "auth.mysql.auth_query", "emqx_auth_mysql.auth_query", [
|
||||
{datatype, string}
|
||||
]}.
|
||||
|
||||
{mapping, "auth.mysql.password_hash", "emqx_auth_mysql.password_hash", [
|
||||
{datatype, string}
|
||||
]}.
|
||||
|
||||
{mapping, "auth.mysql.super_query", "emqx_auth_mysql.super_query", [
|
||||
{datatype, string}
|
||||
]}.
|
||||
|
||||
{mapping, "auth.mysql.acl_query", "emqx_auth_mysql.acl_query", [
|
||||
{datatype, string}
|
||||
]}.
|
||||
|
||||
{translation, "emqx_auth_mysql.password_hash", fun(Conf) ->
|
||||
HashValue = cuttlefish:conf_get("auth.mysql.password_hash", Conf),
|
||||
case string:tokens(HashValue, ",") of
|
||||
[Hash] -> list_to_atom(Hash);
|
||||
[Prefix, Suffix] -> {list_to_atom(Prefix), list_to_atom(Suffix)};
|
||||
[Hash, MacFun, Iterations, Dklen] -> {list_to_atom(Hash), list_to_atom(MacFun), list_to_integer(Iterations), list_to_integer(Dklen)};
|
||||
_ -> plain
|
||||
end
|
||||
end}.
|
|
@ -1,24 +0,0 @@
|
|||
{deps,
|
||||
[
|
||||
{mysql, {git, "https://github.com/emqx/mysql-otp", {tag, "1.7.1"}}}
|
||||
]}.
|
||||
|
||||
{edoc_opts, [{preprocess, true}]}.
|
||||
{erl_opts, [warn_unused_vars,
|
||||
warn_shadow_vars,
|
||||
warn_unused_import,
|
||||
warn_obsolete_guard,
|
||||
debug_info,
|
||||
compressed,
|
||||
{parse_transform}
|
||||
]}.
|
||||
{overrides, [{add, [{erl_opts, [compressed]}]}]}.
|
||||
|
||||
{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}.
|
|
@ -1,119 +0,0 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_acl_mysql).
|
||||
|
||||
-include("emqx_auth_mysql.hrl").
|
||||
|
||||
-include_lib("emqx/include/emqx.hrl").
|
||||
-include_lib("emqx/include/logger.hrl").
|
||||
|
||||
%% ACL Callbacks
|
||||
-export([ register_metrics/0
|
||||
, check_acl/5
|
||||
, description/0
|
||||
]).
|
||||
|
||||
-spec(register_metrics() -> ok).
|
||||
register_metrics() ->
|
||||
lists:foreach(fun emqx_metrics:ensure/1, ?ACL_METRICS).
|
||||
|
||||
check_acl(ClientInfo, PubSub, Topic, NoMatchAction, #{pool := Pool} = State) ->
|
||||
case do_check_acl(Pool, ClientInfo, PubSub, Topic, NoMatchAction, State) of
|
||||
ok -> emqx_metrics:inc(?ACL_METRICS(ignore)), ok;
|
||||
{stop, allow} -> emqx_metrics:inc(?ACL_METRICS(allow)), {stop, allow};
|
||||
{stop, deny} -> emqx_metrics:inc(?ACL_METRICS(deny)), {stop, deny}
|
||||
end.
|
||||
|
||||
do_check_acl(_Pool, #{username := <<$$, _/binary>>}, _PubSub, _Topic, _NoMatchAction, _State) ->
|
||||
ok;
|
||||
do_check_acl(Pool, ClientInfo, PubSub, Topic, _NoMatchAction, #{acl_query := {AclSql, AclParams}}) ->
|
||||
case emqx_auth_mysql_cli:query(Pool, AclSql, AclParams, ClientInfo) of
|
||||
{ok, _Columns, []} -> ok;
|
||||
{ok, _Columns, Rows} ->
|
||||
Rules = filter(PubSub, compile(Rows)),
|
||||
case match(ClientInfo, Topic, Rules) of
|
||||
{matched, allow} -> {stop, allow};
|
||||
{matched, deny} -> {stop, deny};
|
||||
nomatch -> ok
|
||||
end;
|
||||
{error, Reason} ->
|
||||
?LOG(error, "[MySQL] do_check_acl error: ~p~n", [Reason]),
|
||||
ok
|
||||
end.
|
||||
|
||||
match(_ClientInfo, _Topic, []) ->
|
||||
nomatch;
|
||||
|
||||
match(ClientInfo, Topic, [Rule|Rules]) ->
|
||||
case emqx_access_rule:match(ClientInfo, Topic, Rule) of
|
||||
nomatch ->
|
||||
match(ClientInfo, Topic, Rules);
|
||||
{matched, AllowDeny} ->
|
||||
{matched, AllowDeny}
|
||||
end.
|
||||
|
||||
filter(PubSub, Rules) ->
|
||||
[Term || Term = {_, _, Access, _} <- Rules,
|
||||
Access =:= PubSub orelse Access =:= pubsub].
|
||||
|
||||
compile(Rows) ->
|
||||
compile(Rows, []).
|
||||
compile([], Acc) ->
|
||||
Acc;
|
||||
compile([[Allow, IpAddr, Username, ClientId, Access, Topic]|T], Acc) ->
|
||||
Who = who(IpAddr, Username, ClientId),
|
||||
Term = {allow(Allow), Who, access(Access), [topic(Topic)]},
|
||||
compile(T, [emqx_access_rule:compile(Term) | Acc]).
|
||||
|
||||
who(_, <<"$all">>, _) ->
|
||||
all;
|
||||
who(null, null, null) ->
|
||||
throw(undefined_who);
|
||||
who(CIDR, Username, ClientId) ->
|
||||
Cols = [{ipaddr, b2l(CIDR)}, {user, Username}, {client, ClientId}],
|
||||
case [{C, V} || {C, V} <- Cols, not empty(V)] of
|
||||
[Who] -> Who;
|
||||
Conds -> {'and', Conds}
|
||||
end.
|
||||
|
||||
allow(1) -> allow;
|
||||
allow(0) -> deny;
|
||||
allow(<<"1">>) -> allow;
|
||||
allow(<<"0">>) -> deny.
|
||||
|
||||
access(1) -> subscribe;
|
||||
access(2) -> publish;
|
||||
access(3) -> pubsub;
|
||||
access(<<"1">>) -> subscribe;
|
||||
access(<<"2">>) -> publish;
|
||||
access(<<"3">>) -> pubsub.
|
||||
|
||||
topic(<<"eq ", Topic/binary>>) ->
|
||||
{eq, Topic};
|
||||
topic(Topic) ->
|
||||
Topic.
|
||||
|
||||
description() ->
|
||||
"ACL with Mysql".
|
||||
|
||||
b2l(null) -> null;
|
||||
b2l(B) -> binary_to_list(B).
|
||||
|
||||
empty(null) -> true;
|
||||
empty("") -> true;
|
||||
empty(<<>>) -> true;
|
||||
empty(_) -> false.
|
|
@ -1,14 +0,0 @@
|
|||
{application, emqx_auth_mysql,
|
||||
[{description, "EMQ X Authentication/ACL with MySQL"},
|
||||
{vsn, "4.4.0"}, % strict semver, bump manually!
|
||||
{modules, []},
|
||||
{registered, [emqx_auth_mysql_sup]},
|
||||
{applications, [kernel,stdlib,mysql,ecpool]},
|
||||
{mod, {emqx_auth_mysql_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-mysql"}
|
||||
]}
|
||||
]}.
|
|
@ -1,91 +0,0 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_auth_mysql).
|
||||
|
||||
-include("emqx_auth_mysql.hrl").
|
||||
|
||||
-include_lib("emqx/include/emqx.hrl").
|
||||
-include_lib("emqx/include/logger.hrl").
|
||||
-include_lib("emqx/include/types.hrl").
|
||||
|
||||
-export([ register_metrics/0
|
||||
, check/3
|
||||
, description/0
|
||||
]).
|
||||
|
||||
-define(EMPTY(Username), (Username =:= undefined orelse Username =:= <<>>)).
|
||||
|
||||
-spec(register_metrics() -> ok).
|
||||
register_metrics() ->
|
||||
lists:foreach(fun emqx_metrics:ensure/1, ?AUTH_METRICS).
|
||||
|
||||
check(ClientInfo = #{password := Password}, AuthResult,
|
||||
#{auth_query := {AuthSql, AuthParams},
|
||||
super_query := SuperQuery,
|
||||
hash_type := HashType,
|
||||
pool := Pool}) ->
|
||||
CheckPass = case emqx_auth_mysql_cli:query(Pool, AuthSql, AuthParams, ClientInfo) of
|
||||
{ok, [<<"password">>], [[PassHash]]} ->
|
||||
check_pass({PassHash, Password}, HashType);
|
||||
{ok, [<<"password">>, <<"salt">>], [[PassHash, Salt]]} ->
|
||||
check_pass({PassHash, Salt, Password}, HashType);
|
||||
{ok, _Columns, []} ->
|
||||
{error, not_found};
|
||||
{error, Reason} ->
|
||||
?LOG(error, "[MySQL] query '~p' failed: ~p", [AuthSql, Reason]),
|
||||
{error, Reason}
|
||||
end,
|
||||
case CheckPass of
|
||||
ok ->
|
||||
emqx_metrics:inc(?AUTH_METRICS(success)),
|
||||
{stop, AuthResult#{is_superuser => is_superuser(Pool, SuperQuery, ClientInfo),
|
||||
anonymous => false,
|
||||
auth_result => success}};
|
||||
{error, not_found} ->
|
||||
emqx_metrics:inc(?AUTH_METRICS(ignore)), ok;
|
||||
{error, ResultCode} ->
|
||||
?LOG(error, "[MySQL] Auth from mysql failed: ~p", [ResultCode]),
|
||||
emqx_metrics:inc(?AUTH_METRICS(failure)),
|
||||
{stop, AuthResult#{auth_result => ResultCode, anonymous => false}}
|
||||
end.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Is Superuser?
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-spec(is_superuser(atom(), maybe({string(), list()}), emqx_types:client()) -> boolean()).
|
||||
is_superuser(_Pool, undefined, _ClientInfo) -> false;
|
||||
is_superuser(Pool, {SuperSql, Params}, ClientInfo) ->
|
||||
case emqx_auth_mysql_cli:query(Pool, SuperSql, Params, ClientInfo) of
|
||||
{ok, [_Super], [[1]]} ->
|
||||
true;
|
||||
{ok, [_Super], [[_False]]} ->
|
||||
false;
|
||||
{ok, [_Super], []} ->
|
||||
false;
|
||||
{error, _Error} ->
|
||||
false
|
||||
end.
|
||||
|
||||
check_pass(Password, HashType) ->
|
||||
case emqx_passwd:check_pass(Password, HashType) of
|
||||
ok -> ok;
|
||||
{error, _Reason} -> {error, not_authorized}
|
||||
end.
|
||||
|
||||
description() -> "Authentication with MySQL".
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_auth_mysql_app).
|
||||
|
||||
-behaviour(application).
|
||||
|
||||
-emqx_plugin(auth).
|
||||
|
||||
-include("emqx_auth_mysql.hrl").
|
||||
|
||||
-import(emqx_auth_mysql_cli, [parse_query/1]).
|
||||
|
||||
%% Application callbacks
|
||||
-export([ start/2
|
||||
, prep_stop/1
|
||||
, stop/1
|
||||
]).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Application callbacks
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
start(_StartType, _StartArgs) ->
|
||||
{ok, Sup} = emqx_auth_mysql_sup:start_link(),
|
||||
_ = if_enabled(auth_query, fun load_auth_hook/1),
|
||||
_ = if_enabled(acl_query, fun load_acl_hook/1),
|
||||
|
||||
{ok, Sup}.
|
||||
|
||||
prep_stop(State) ->
|
||||
emqx:unhook('client.authenticate', {emqx_auth_mysql, check}),
|
||||
emqx:unhook('client.check_acl', {emqx_acl_mysql, check_acl}),
|
||||
State.
|
||||
|
||||
stop(_State) ->
|
||||
ok.
|
||||
|
||||
load_auth_hook(AuthQuery) ->
|
||||
ok = emqx_auth_mysql:register_metrics(),
|
||||
SuperQuery = parse_query(application:get_env(?APP, super_query, undefined)),
|
||||
{ok, HashType} = application:get_env(?APP, password_hash),
|
||||
Params = #{auth_query => AuthQuery,
|
||||
super_query => SuperQuery,
|
||||
hash_type => HashType,
|
||||
pool => ?APP},
|
||||
emqx:hook('client.authenticate', {emqx_auth_mysql, check, [Params]}).
|
||||
|
||||
load_acl_hook(AclQuery) ->
|
||||
ok = emqx_acl_mysql:register_metrics(),
|
||||
emqx:hook('client.check_acl', {emqx_acl_mysql, check_acl, [#{acl_query => AclQuery, pool =>?APP}]}).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Internal function
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
if_enabled(Cfg, Fun) ->
|
||||
case application:get_env(?APP, Cfg) of
|
||||
{ok, Query} -> Fun(parse_query(Query));
|
||||
undefined -> ok
|
||||
end.
|
|
@ -1,91 +0,0 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_auth_mysql_cli).
|
||||
|
||||
-behaviour(ecpool_worker).
|
||||
|
||||
-include("emqx_auth_mysql.hrl").
|
||||
-include_lib("emqx/include/emqx.hrl").
|
||||
-include_lib("emqx/include/logger.hrl").
|
||||
|
||||
-export([ parse_query/1
|
||||
, connect/1
|
||||
, query/4
|
||||
]).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Avoid SQL Injection: Parse SQL to Parameter Query.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
parse_query(undefined) ->
|
||||
undefined;
|
||||
parse_query(Sql) ->
|
||||
case re:run(Sql, "'%[ucCad]'", [global, {capture, all, list}]) of
|
||||
{match, Variables} ->
|
||||
Params = [Var || [Var] <- Variables],
|
||||
{re:replace(Sql, "'%[ucCad]'", "?", [global, {return, list}]), Params};
|
||||
nomatch ->
|
||||
{Sql, []}
|
||||
end.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% MySQL Connect/Query
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
connect(Options) ->
|
||||
case mysql:start_link(Options) of
|
||||
{ok, Pid} -> {ok, Pid};
|
||||
ignore -> {error, ignore};
|
||||
{error, Reason = {{_, {error, econnrefused}}, _}} ->
|
||||
?LOG(error, "[MySQL] Can't connect to MySQL server: Connection refused."),
|
||||
{error, Reason};
|
||||
{error, Reason = {ErrorCode, _, Error}} ->
|
||||
?LOG(error, "[MySQL] Can't connect to MySQL server: ~p - ~p", [ErrorCode, Error]),
|
||||
{error, Reason};
|
||||
{error, Reason} ->
|
||||
?LOG(error, "[MySQL] Can't connect to MySQL server: ~p", [Reason]),
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
query(Pool, Sql, Params, ClientInfo) ->
|
||||
ecpool:with_client(Pool, fun(C) -> mysql:query(C, Sql, replvar(Params, ClientInfo)) end).
|
||||
|
||||
replvar(Params, ClientInfo) ->
|
||||
replvar(Params, ClientInfo, []).
|
||||
|
||||
replvar([], _ClientInfo, Acc) ->
|
||||
lists:reverse(Acc);
|
||||
|
||||
replvar(["'%u'" | Params], ClientInfo, Acc) ->
|
||||
replvar(Params, ClientInfo, [safe_get(username, ClientInfo) | Acc]);
|
||||
replvar(["'%c'" | Params], ClientInfo = #{clientid := ClientId}, Acc) ->
|
||||
replvar(Params, ClientInfo, [ClientId | Acc]);
|
||||
replvar(["'%a'" | Params], ClientInfo = #{peerhost := IpAddr}, Acc) ->
|
||||
replvar(Params, ClientInfo, [inet_parse:ntoa(IpAddr) | Acc]);
|
||||
replvar(["'%C'" | Params], ClientInfo, Acc) ->
|
||||
replvar(Params, ClientInfo, [safe_get(cn, ClientInfo)| Acc]);
|
||||
replvar(["'%d'" | Params], ClientInfo, Acc) ->
|
||||
replvar(Params, ClientInfo, [safe_get(dn, ClientInfo)| Acc]);
|
||||
replvar([Param | Params], ClientInfo, Acc) ->
|
||||
replvar(Params, ClientInfo, [Param | Acc]).
|
||||
|
||||
safe_get(K, ClientInfo) ->
|
||||
bin(maps:get(K, ClientInfo, "undefined")).
|
||||
|
||||
bin(A) when is_atom(A) -> atom_to_binary(A, utf8);
|
||||
bin(B) when is_binary(B) -> B;
|
||||
bin(X) -> X.
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue