Merge pull request #5346 from tigercl/feat/mongo-auhtn
feat(authn): support mongodb authn
This commit is contained in:
commit
59f645dc59
|
@ -6,6 +6,21 @@ emqx_authn: {
|
||||||
# mechanism: password-based
|
# mechanism: password-based
|
||||||
# server_type: built-in-database
|
# server_type: built-in-database
|
||||||
# user_id_type: clientid
|
# user_id_type: clientid
|
||||||
|
# },
|
||||||
|
# {
|
||||||
|
# name: "authenticator2"
|
||||||
|
# mechanism: password-based
|
||||||
|
# server_type: mongodb
|
||||||
|
# server: "127.0.0.1:27017"
|
||||||
|
# database: mqtt
|
||||||
|
# collection: users
|
||||||
|
# selector: {
|
||||||
|
# username: "${mqtt-username}"
|
||||||
|
# }
|
||||||
|
# password_hash_field: password_hash
|
||||||
|
# salt_field: salt
|
||||||
|
# password_hash_algorithm: sha256
|
||||||
|
# salt_position: prefix
|
||||||
# }
|
# }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,8 @@
|
||||||
-define(VER_1, <<"1">>).
|
-define(VER_1, <<"1">>).
|
||||||
-define(VER_2, <<"2">>).
|
-define(VER_2, <<"2">>).
|
||||||
|
|
||||||
|
-define(RE_PLACEHOLDER, "\\$\\{[a-z0-9\\-]+\\}").
|
||||||
|
|
||||||
-record(authenticator,
|
-record(authenticator,
|
||||||
{ id :: binary()
|
{ id :: binary()
|
||||||
, name :: binary()
|
, name :: binary()
|
||||||
|
@ -35,25 +37,3 @@
|
||||||
}).
|
}).
|
||||||
|
|
||||||
-define(AUTH_SHARD, emqx_authn_shard).
|
-define(AUTH_SHARD, emqx_authn_shard).
|
||||||
|
|
||||||
-define(CLUSTER_CALL(Module, Func, Args), ?CLUSTER_CALL(Module, Func, Args, ok)).
|
|
||||||
|
|
||||||
-define(CLUSTER_CALL(Module, Func, Args, ResParttern),
|
|
||||||
fun() ->
|
|
||||||
case LocalResult = erlang:apply(Module, Func, Args) of
|
|
||||||
ResParttern ->
|
|
||||||
Nodes = nodes(),
|
|
||||||
{ResL, BadNodes} = rpc:multicall(Nodes, Module, Func, Args, 5000),
|
|
||||||
NResL = lists:zip(Nodes - BadNodes, ResL),
|
|
||||||
Errors = lists:filter(fun({_, ResParttern}) -> false;
|
|
||||||
(_) -> true
|
|
||||||
end, NResL),
|
|
||||||
OtherErrors = [{BadNode, node_does_not_exist} || BadNode <- BadNodes],
|
|
||||||
case Errors ++ OtherErrors of
|
|
||||||
[] -> LocalResult;
|
|
||||||
NErrors -> {error, NErrors}
|
|
||||||
end;
|
|
||||||
ErrorResult ->
|
|
||||||
{error, ErrorResult}
|
|
||||||
end
|
|
||||||
end()).
|
|
||||||
|
|
|
@ -314,6 +314,8 @@ authenticator_provider(#{mechanism := 'password-based', server_type := 'mysql'})
|
||||||
emqx_authn_mysql;
|
emqx_authn_mysql;
|
||||||
authenticator_provider(#{mechanism := 'password-based', server_type := 'pgsql'}) ->
|
authenticator_provider(#{mechanism := 'password-based', server_type := 'pgsql'}) ->
|
||||||
emqx_authn_pgsql;
|
emqx_authn_pgsql;
|
||||||
|
authenticator_provider(#{mechanism := 'password-based', server_type := 'mongodb'}) ->
|
||||||
|
emqx_authn_mongodb;
|
||||||
authenticator_provider(#{mechanism := 'password-based', server_type := 'http-server'}) ->
|
authenticator_provider(#{mechanism := 'password-based', server_type := 'http-server'}) ->
|
||||||
emqx_authn_http;
|
emqx_authn_http;
|
||||||
authenticator_provider(#{mechanism := jwt}) ->
|
authenticator_provider(#{mechanism := jwt}) ->
|
||||||
|
|
|
@ -775,7 +775,11 @@ definitions() ->
|
||||||
default => true
|
default => true
|
||||||
},
|
},
|
||||||
ssl => minirest:ref(<<"ssl">>),
|
ssl => minirest:ref(<<"ssl">>),
|
||||||
password_hash_algorithm => minirest:ref(<<"password_hash_algorithm">>),
|
password_hash_algorithm => #{
|
||||||
|
type => string,
|
||||||
|
enum => [<<"plain">>, <<"md5">>, <<"sha">>, <<"sha256">>, <<"sha512">>, <<"bcrypt">>],
|
||||||
|
default => <<"sha256">>
|
||||||
|
},
|
||||||
salt_position => #{
|
salt_position => #{
|
||||||
type => string,
|
type => string,
|
||||||
enum => [<<"prefix">>, <<"suffix">>],
|
enum => [<<"prefix">>, <<"suffix">>],
|
||||||
|
@ -822,7 +826,11 @@ definitions() ->
|
||||||
type => boolean,
|
type => boolean,
|
||||||
default => true
|
default => true
|
||||||
},
|
},
|
||||||
password_hash_algorithm => minirest:ref(<<"password_hash_algorithm">>),
|
password_hash_algorithm => #{
|
||||||
|
type => string,
|
||||||
|
enum => [<<"plain">>, <<"md5">>, <<"sha">>, <<"sha256">>, <<"sha512">>, <<"bcrypt">>],
|
||||||
|
default => <<"sha256">>
|
||||||
|
},
|
||||||
salt_position => #{
|
salt_position => #{
|
||||||
type => string,
|
type => string,
|
||||||
enum => [<<"prefix">>, <<"suffix">>],
|
enum => [<<"prefix">>, <<"suffix">>],
|
||||||
|
|
|
@ -47,6 +47,9 @@ authenticators(type) ->
|
||||||
hoconsc:array({union, [ hoconsc:ref(emqx_authn_mnesia, config)
|
hoconsc:array({union, [ hoconsc:ref(emqx_authn_mnesia, config)
|
||||||
, hoconsc:ref(emqx_authn_mysql, config)
|
, hoconsc:ref(emqx_authn_mysql, config)
|
||||||
, hoconsc:ref(emqx_authn_pgsql, config)
|
, hoconsc:ref(emqx_authn_pgsql, config)
|
||||||
|
, hoconsc:ref(emqx_authn_mongodb, standalone)
|
||||||
|
, hoconsc:ref(emqx_authn_mongodb, 'replica-set')
|
||||||
|
, hoconsc:ref(emqx_authn_mongodb, sharded)
|
||||||
, hoconsc:ref(emqx_authn_http, get)
|
, hoconsc:ref(emqx_authn_http, get)
|
||||||
, hoconsc:ref(emqx_authn_http, post)
|
, hoconsc:ref(emqx_authn_http, post)
|
||||||
, hoconsc:ref(emqx_authn_jwt, 'hmac-based')
|
, hoconsc:ref(emqx_authn_jwt, 'hmac-based')
|
||||||
|
|
|
@ -16,36 +16,53 @@
|
||||||
|
|
||||||
-module(emqx_authn_utils).
|
-module(emqx_authn_utils).
|
||||||
|
|
||||||
-export([ replace_placeholder/2
|
-export([ replace_placeholders/2
|
||||||
|
, replace_placeholder/2
|
||||||
, gen_salt/0
|
, gen_salt/0
|
||||||
|
, bin/1
|
||||||
]).
|
]).
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
%% APIs
|
%% APIs
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
replace_placeholder(PlaceHolders, Data) ->
|
replace_placeholders(PlaceHolders, Data) ->
|
||||||
replace_placeholder(PlaceHolders, Data, []).
|
replace_placeholders(PlaceHolders, Data, []).
|
||||||
|
|
||||||
replace_placeholder([], _Data, Acc) ->
|
replace_placeholders([], _Credential, Acc) ->
|
||||||
lists:reverse(Acc);
|
lists:reverse(Acc);
|
||||||
replace_placeholder([<<"${mqtt-username}">> | More], #{username := Username} = Data, Acc) ->
|
replace_placeholders([Placeholder | More], Credential, Acc) ->
|
||||||
replace_placeholder(More, Data, [convert_to_sql_param(Username) | Acc]);
|
case replace_placeholder(Placeholder, Credential) of
|
||||||
replace_placeholder([<<"${mqtt-clientid}">> | More], #{clientid := ClientID} = Data, Acc) ->
|
undefined ->
|
||||||
replace_placeholder(More, Data, [convert_to_sql_param(ClientID) | Acc]);
|
error({cannot_get_variable, Placeholder});
|
||||||
replace_placeholder([<<"${ip-address}">> | More], #{peerhost := IPAddress} = Data, Acc) ->
|
V ->
|
||||||
replace_placeholder(More, Data, [convert_to_sql_param(IPAddress) | Acc]);
|
replace_placeholders(More, Credential, [convert_to_sql_param(V) | Acc])
|
||||||
replace_placeholder([<<"${cert-subject}">> | More], #{dn := Subject} = Data, Acc) ->
|
end.
|
||||||
replace_placeholder(More, Data, [convert_to_sql_param(Subject) | Acc]);
|
|
||||||
replace_placeholder([<<"${cert-common-name}">> | More], #{cn := CommonName} = Data, Acc) ->
|
replace_placeholder(<<"${mqtt-username}">>, Credential) ->
|
||||||
replace_placeholder(More, Data, [convert_to_sql_param(CommonName) | Acc]);
|
maps:get(username, Credential, undefined);
|
||||||
replace_placeholder([_ | More], Data, Acc) ->
|
replace_placeholder(<<"${mqtt-clientid}">>, Credential) ->
|
||||||
replace_placeholder(More, Data, [null | Acc]).
|
maps:get(clientid, Credential, undefined);
|
||||||
|
replace_placeholder(<<"${mqtt-password}">>, Credential) ->
|
||||||
|
maps:get(password, Credential, undefined);
|
||||||
|
replace_placeholder(<<"${ip-address}">>, Credential) ->
|
||||||
|
maps:get(peerhost, Credential, undefined);
|
||||||
|
replace_placeholder(<<"${cert-subject}">>, Credential) ->
|
||||||
|
maps:get(dn, Credential, undefined);
|
||||||
|
replace_placeholder(<<"${cert-common-name}">>, Credential) ->
|
||||||
|
maps:get(cn, Credential, undefined);
|
||||||
|
replace_placeholder(Constant, _) ->
|
||||||
|
Constant.
|
||||||
|
|
||||||
|
|
||||||
gen_salt() ->
|
gen_salt() ->
|
||||||
<<X:128/big-unsigned-integer>> = crypto:strong_rand_bytes(16),
|
<<X:128/big-unsigned-integer>> = crypto:strong_rand_bytes(16),
|
||||||
iolist_to_binary(io_lib:format("~32.16.0b", [X])).
|
iolist_to_binary(io_lib:format("~32.16.0b", [X])).
|
||||||
|
|
||||||
|
bin(A) when is_atom(A) -> atom_to_binary(A, utf8);
|
||||||
|
bin(L) when is_list(L) -> list_to_binary(L);
|
||||||
|
bin(X) -> X.
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
%% Internal functions
|
%% Internal functions
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
@ -54,7 +71,3 @@ convert_to_sql_param(undefined) ->
|
||||||
null;
|
null;
|
||||||
convert_to_sql_param(V) ->
|
convert_to_sql_param(V) ->
|
||||||
bin(V).
|
bin(V).
|
||||||
|
|
||||||
bin(A) when is_atom(A) -> atom_to_binary(A, utf8);
|
|
||||||
bin(L) when is_list(L) -> list_to_binary(L);
|
|
||||||
bin(X) -> X.
|
|
||||||
|
|
|
@ -132,7 +132,6 @@ destroy(#{user_group := UserGroup}) ->
|
||||||
end, mnesia:select(?TAB, MatchSpec, write))
|
end, mnesia:select(?TAB, MatchSpec, write))
|
||||||
end).
|
end).
|
||||||
|
|
||||||
%% TODO: binary to atom
|
|
||||||
add_user(#{user_id := UserID,
|
add_user(#{user_id := UserID,
|
||||||
password := Password}, #{user_group := UserGroup} = State) ->
|
password := Password}, #{user_group := UserGroup} = State) ->
|
||||||
trans(
|
trans(
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
-module(emqx_authn_http).
|
-module(emqx_authn_http).
|
||||||
|
|
||||||
-include("emqx_authn.hrl").
|
-include("emqx_authn.hrl").
|
||||||
|
-include_lib("emqx/include/logger.hrl").
|
||||||
-include_lib("typerefl/include/types.hrl").
|
-include_lib("typerefl/include/types.hrl").
|
||||||
|
|
||||||
-behaviour(hocon_schema).
|
-behaviour(hocon_schema).
|
||||||
|
@ -122,15 +123,16 @@ create(#{ method := Method
|
||||||
, headers => normalize_headers(Headers)
|
, headers => normalize_headers(Headers)
|
||||||
, form_data => maps:to_list(FormData)
|
, form_data => maps:to_list(FormData)
|
||||||
, request_timeout => RequestTimeout
|
, request_timeout => RequestTimeout
|
||||||
|
, '_unique' => Unique
|
||||||
},
|
},
|
||||||
case emqx_resource:create_local(Unique,
|
case emqx_resource:create_local(Unique,
|
||||||
emqx_connector_http,
|
emqx_connector_http,
|
||||||
Config#{base_url => maps:remove(query, URIMap),
|
Config#{base_url => maps:remove(query, URIMap),
|
||||||
pool_type => random}) of
|
pool_type => random}) of
|
||||||
{ok, _} ->
|
{ok, _} ->
|
||||||
{ok, State#{resource_id => Unique}};
|
{ok, State};
|
||||||
{error, already_created} ->
|
{error, already_created} ->
|
||||||
{ok, State#{resource_id => Unique}};
|
{ok, State};
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
{error, Reason}
|
{error, Reason}
|
||||||
end.
|
end.
|
||||||
|
@ -146,11 +148,12 @@ update(Config, State) ->
|
||||||
|
|
||||||
authenticate(#{auth_method := _}, _) ->
|
authenticate(#{auth_method := _}, _) ->
|
||||||
ignore;
|
ignore;
|
||||||
authenticate(Credential, #{resource_id := ResourceID,
|
authenticate(Credential, #{'_unique' := Unique,
|
||||||
method := Method,
|
method := Method,
|
||||||
request_timeout := RequestTimeout} = State) ->
|
request_timeout := RequestTimeout} = State) ->
|
||||||
|
try
|
||||||
Request = generate_request(Credential, State),
|
Request = generate_request(Credential, State),
|
||||||
case emqx_resource:query(ResourceID, {Method, Request, RequestTimeout}) of
|
case emqx_resource:query(Unique, {Method, Request, RequestTimeout}) of
|
||||||
{ok, 204, _Headers} -> ok;
|
{ok, 204, _Headers} -> ok;
|
||||||
{ok, 200, Headers, Body} ->
|
{ok, 200, Headers, Body} ->
|
||||||
ContentType = proplists:get_value(<<"content-type">>, Headers, <<"application/json">>),
|
ContentType = proplists:get_value(<<"content-type">>, Headers, <<"application/json">>),
|
||||||
|
@ -163,10 +166,15 @@ authenticate(Credential, #{resource_id := ResourceID,
|
||||||
end;
|
end;
|
||||||
{error, _Reason} ->
|
{error, _Reason} ->
|
||||||
ignore
|
ignore
|
||||||
|
end
|
||||||
|
catch
|
||||||
|
error:Reason ->
|
||||||
|
?LOG(warning, "The following error occurred in '~s' during authentication: ~p", [Unique, Reason]),
|
||||||
|
ignore
|
||||||
end.
|
end.
|
||||||
|
|
||||||
destroy(#{resource_id := ResourceID}) ->
|
destroy(#{'_unique' := Unique}) ->
|
||||||
_ = emqx_resource:remove_local(ResourceID),
|
_ = emqx_resource:remove_local(Unique),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
@ -242,31 +250,18 @@ generate_request(Credential, #{method := Method,
|
||||||
{NPath, Headers, Body}
|
{NPath, Headers, Body}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
replace_placeholders(FormData0, Credential) ->
|
replace_placeholders(KVs, Credential) ->
|
||||||
FormData = lists:map(fun({K, V0}) ->
|
replace_placeholders(KVs, Credential, []).
|
||||||
case replace_placeholder(V0, Credential) of
|
|
||||||
undefined -> {K, undefined};
|
|
||||||
V -> {K, bin(V)}
|
|
||||||
end
|
|
||||||
end, FormData0),
|
|
||||||
lists:filter(fun({_, V}) ->
|
|
||||||
V =/= undefined
|
|
||||||
end, FormData).
|
|
||||||
|
|
||||||
replace_placeholder(<<"${mqtt-username}">>, Credential) ->
|
replace_placeholders([], _Credential, Acc) ->
|
||||||
maps:get(username, Credential, undefined);
|
lists:reverse(Acc);
|
||||||
replace_placeholder(<<"${mqtt-clientid}">>, Credential) ->
|
replace_placeholders([{K, V0} | More], Credential, Acc) ->
|
||||||
maps:get(clientid, Credential, undefined);
|
case emqx_authn_utils:replace_placeholder(V0, Credential) of
|
||||||
replace_placeholder(<<"${mqtt-password}">>, Credential) ->
|
undefined ->
|
||||||
maps:get(password, Credential, undefined);
|
error({cannot_get_variable, V0});
|
||||||
replace_placeholder(<<"${ip-address}">>, Credential) ->
|
V ->
|
||||||
maps:get(peerhost, Credential, undefined);
|
replace_placeholders(More, Credential, [{K, emqx_authn_utils:bin(V)} | Acc])
|
||||||
replace_placeholder(<<"${cert-subject}">>, Credential) ->
|
end.
|
||||||
maps:get(dn, Credential, undefined);
|
|
||||||
replace_placeholder(<<"${cert-common-name}">>, Credential) ->
|
|
||||||
maps:get(cn, Credential, undefined);
|
|
||||||
replace_placeholder(Constant, _) ->
|
|
||||||
Constant.
|
|
||||||
|
|
||||||
append_query(Path, []) ->
|
append_query(Path, []) ->
|
||||||
Path;
|
Path;
|
||||||
|
@ -301,7 +296,3 @@ parse_body(<<"application/x-www-form-urlencoded">>, Body) ->
|
||||||
{ok, cow_qs:parse_qs(Body)};
|
{ok, cow_qs:parse_qs(Body)};
|
||||||
parse_body(ContentType, _) ->
|
parse_body(ContentType, _) ->
|
||||||
{error, {unsupported_content_type, ContentType}}.
|
{error, {unsupported_content_type, ContentType}}.
|
||||||
|
|
||||||
bin(A) when is_atom(A) -> atom_to_binary(A, utf8);
|
|
||||||
bin(L) when is_list(L) -> list_to_binary(L);
|
|
||||||
bin(X) -> X.
|
|
|
@ -0,0 +1,227 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% 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_authn_mongodb).
|
||||||
|
|
||||||
|
-include("emqx_authn.hrl").
|
||||||
|
-include_lib("emqx/include/logger.hrl").
|
||||||
|
-include_lib("typerefl/include/types.hrl").
|
||||||
|
|
||||||
|
-behaviour(hocon_schema).
|
||||||
|
|
||||||
|
-export([ structs/0
|
||||||
|
, fields/1
|
||||||
|
]).
|
||||||
|
|
||||||
|
-export([ create/1
|
||||||
|
, update/2
|
||||||
|
, authenticate/2
|
||||||
|
, destroy/1
|
||||||
|
]).
|
||||||
|
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
%% Hocon Schema
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
structs() -> [""].
|
||||||
|
|
||||||
|
fields("") ->
|
||||||
|
[ {config, {union, [ hoconsc:t(standalone)
|
||||||
|
, hoconsc:t('replica-set')
|
||||||
|
, hoconsc:t(sharded)
|
||||||
|
]}}
|
||||||
|
];
|
||||||
|
|
||||||
|
fields(standalone) ->
|
||||||
|
common_fields() ++ emqx_connector_mongo:fields(single);
|
||||||
|
|
||||||
|
fields('replica-set') ->
|
||||||
|
common_fields() ++ emqx_connector_mongo:fields(rs);
|
||||||
|
|
||||||
|
fields(sharded) ->
|
||||||
|
common_fields() ++ emqx_connector_mongo:fields(sharded).
|
||||||
|
|
||||||
|
common_fields() ->
|
||||||
|
[ {name, fun emqx_authn_schema:authenticator_name/1}
|
||||||
|
, {mechanism, {enum, ['password-based']}}
|
||||||
|
, {server_type, {enum, [mongodb]}}
|
||||||
|
, {collection, fun collection/1}
|
||||||
|
, {selector, fun selector/1}
|
||||||
|
, {password_hash_field, fun password_hash_field/1}
|
||||||
|
, {salt_field, fun salt_field/1}
|
||||||
|
, {password_hash_algorithm, fun password_hash_algorithm/1}
|
||||||
|
, {salt_position, fun salt_position/1}
|
||||||
|
].
|
||||||
|
|
||||||
|
collection(type) -> binary();
|
||||||
|
collection(nullable) -> false;
|
||||||
|
collection(_) -> undefined.
|
||||||
|
|
||||||
|
selector(type) -> map();
|
||||||
|
selector(nullable) -> false;
|
||||||
|
selector(_) -> undefined.
|
||||||
|
|
||||||
|
password_hash_field(type) -> binary();
|
||||||
|
password_hash_field(nullable) -> false;
|
||||||
|
password_hash_field(_) -> undefined.
|
||||||
|
|
||||||
|
salt_field(type) -> binary();
|
||||||
|
salt_field(nullable) -> true;
|
||||||
|
salt_field(_) -> undefined.
|
||||||
|
|
||||||
|
password_hash_algorithm(type) -> {enum, [plain, md5, sha, sha256, sha512, bcrypt]};
|
||||||
|
password_hash_algorithm(default) -> sha256;
|
||||||
|
password_hash_algorithm(_) -> undefined.
|
||||||
|
|
||||||
|
salt_position(type) -> {enum, [prefix, suffix]};
|
||||||
|
salt_position(default) -> prefix;
|
||||||
|
salt_position(_) -> undefined.
|
||||||
|
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
%% APIs
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
create(#{ selector := Selector
|
||||||
|
, '_unique' := Unique
|
||||||
|
} = Config) ->
|
||||||
|
NSelector = parse_selector(Selector),
|
||||||
|
State = maps:with([ collection
|
||||||
|
, password_hash_field
|
||||||
|
, salt_field
|
||||||
|
, password_hash_algorithm
|
||||||
|
, salt_position
|
||||||
|
, '_unique'], Config),
|
||||||
|
NState = State#{selector => NSelector},
|
||||||
|
case emqx_resource:create_local(Unique, emqx_connector_mongo, Config) of
|
||||||
|
{ok, _} ->
|
||||||
|
{ok, NState};
|
||||||
|
{error, already_created} ->
|
||||||
|
{ok, NState};
|
||||||
|
{error, Reason} ->
|
||||||
|
{error, Reason}
|
||||||
|
end.
|
||||||
|
|
||||||
|
update(Config, State) ->
|
||||||
|
case create(Config) of
|
||||||
|
{ok, NewState} ->
|
||||||
|
ok = destroy(State),
|
||||||
|
{ok, NewState};
|
||||||
|
{error, Reason} ->
|
||||||
|
{error, Reason}
|
||||||
|
end.
|
||||||
|
|
||||||
|
authenticate(#{auth_method := _}, _) ->
|
||||||
|
ignore;
|
||||||
|
authenticate(#{password := Password} = Credential,
|
||||||
|
#{ collection := Collection
|
||||||
|
, selector := Selector0
|
||||||
|
, '_unique' := Unique
|
||||||
|
} = State) ->
|
||||||
|
try
|
||||||
|
Selector1 = replace_placeholders(Selector0, Credential),
|
||||||
|
Selector2 = normalize_selector(Selector1),
|
||||||
|
case emqx_resource:query(Unique, {find_one, Collection, Selector2, #{}}) of
|
||||||
|
undefined -> ignore;
|
||||||
|
{error, Reason} ->
|
||||||
|
?LOG(error, "['~s'] Query failed: ~p", [Unique, Reason]),
|
||||||
|
ignore;
|
||||||
|
Doc ->
|
||||||
|
case check_password(Password, Doc, State) of
|
||||||
|
ok -> ok;
|
||||||
|
{error, {cannot_find_password_hash_field, PasswordHashField}} ->
|
||||||
|
?LOG(error, "['~s'] Can't find password hash field: ~s", [Unique, PasswordHashField]),
|
||||||
|
{error, bad_username_or_password};
|
||||||
|
{error, Reason} ->
|
||||||
|
{error, Reason}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
catch
|
||||||
|
error:Error ->
|
||||||
|
?LOG(warning, "The following error occurred in '~s' during authentication: ~p", [Unique, Error]),
|
||||||
|
ignore
|
||||||
|
end.
|
||||||
|
|
||||||
|
destroy(#{'_unique' := Unique}) ->
|
||||||
|
_ = emqx_resource:remove_local(Unique),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
%% Internal functions
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
parse_selector(Selector) ->
|
||||||
|
NSelector = emqx_json:encode(Selector),
|
||||||
|
Tokens = re:split(NSelector, "(" ++ ?RE_PLACEHOLDER ++ ")", [{return, binary}, group, trim]),
|
||||||
|
parse_selector(Tokens, []).
|
||||||
|
|
||||||
|
parse_selector([], Acc) ->
|
||||||
|
lists:reverse(Acc);
|
||||||
|
parse_selector([[Constant, Placeholder] | Tokens], Acc) ->
|
||||||
|
parse_selector(Tokens, [{placeholder, Placeholder}, {constant, Constant} | Acc]);
|
||||||
|
parse_selector([[Constant] | Tokens], Acc) ->
|
||||||
|
parse_selector(Tokens, [{constant, Constant} | Acc]).
|
||||||
|
|
||||||
|
replace_placeholders(Selector, Credential) ->
|
||||||
|
lists:map(fun({constant, Constant}) ->
|
||||||
|
Constant;
|
||||||
|
({placeholder, Placeholder}) ->
|
||||||
|
case emqx_authn_utils:replace_placeholder(Placeholder, Credential) of
|
||||||
|
undefined -> error({cannot_get_variable, Placeholder});
|
||||||
|
Value -> Value
|
||||||
|
end
|
||||||
|
end, Selector).
|
||||||
|
|
||||||
|
normalize_selector(Selector) ->
|
||||||
|
emqx_json:decode(iolist_to_binary(Selector), [return_maps]).
|
||||||
|
|
||||||
|
check_password(undefined, _Selected, _State) ->
|
||||||
|
{error, bad_username_or_password};
|
||||||
|
check_password(Password,
|
||||||
|
Doc,
|
||||||
|
#{password_hash_algorithm := bcrypt,
|
||||||
|
password_hash_field := PasswordHashField}) ->
|
||||||
|
case maps:get(PasswordHashField, Doc, undefined) of
|
||||||
|
undefined ->
|
||||||
|
{error, {cannot_find_password_hash_field, PasswordHashField}};
|
||||||
|
Hash ->
|
||||||
|
case {ok, Hash} =:= bcrypt:hashpw(Password, Hash) of
|
||||||
|
true -> ok;
|
||||||
|
false -> {error, bad_username_or_password}
|
||||||
|
end
|
||||||
|
end;
|
||||||
|
check_password(Password,
|
||||||
|
Doc,
|
||||||
|
#{password_hash_algorithm := Algorithm,
|
||||||
|
password_hash_field := PasswordHashField,
|
||||||
|
salt_position := SaltPosition} = State) ->
|
||||||
|
case maps:get(PasswordHashField, Doc, undefined) of
|
||||||
|
undefined ->
|
||||||
|
{error, {cannot_find_password_hash_field, PasswordHashField}};
|
||||||
|
Hash ->
|
||||||
|
Salt = case maps:get(salt_field, State, undefined) of
|
||||||
|
undefined -> <<>>;
|
||||||
|
SaltField -> maps:get(SaltField, Doc, <<>>)
|
||||||
|
end,
|
||||||
|
case Hash =:= hash(Algorithm, Password, Salt, SaltPosition) of
|
||||||
|
true -> ok;
|
||||||
|
false -> {error, bad_username_or_password}
|
||||||
|
end
|
||||||
|
end.
|
||||||
|
|
||||||
|
hash(Algorithm, Password, Salt, prefix) ->
|
||||||
|
emqx_passwd:hash(Algorithm, <<Salt/binary, Password/binary>>);
|
||||||
|
hash(Algorithm, Password, Salt, suffix) ->
|
||||||
|
emqx_passwd:hash(Algorithm, <<Password/binary, Salt/binary>>).
|
|
@ -17,6 +17,7 @@
|
||||||
-module(emqx_authn_mysql).
|
-module(emqx_authn_mysql).
|
||||||
|
|
||||||
-include("emqx_authn.hrl").
|
-include("emqx_authn.hrl").
|
||||||
|
-include_lib("emqx/include/logger.hrl").
|
||||||
-include_lib("typerefl/include/types.hrl").
|
-include_lib("typerefl/include/types.hrl").
|
||||||
|
|
||||||
-behaviour(hocon_schema).
|
-behaviour(hocon_schema).
|
||||||
|
@ -46,25 +47,12 @@ fields(config) ->
|
||||||
, {query, fun query/1}
|
, {query, fun query/1}
|
||||||
, {query_timeout, fun query_timeout/1}
|
, {query_timeout, fun query_timeout/1}
|
||||||
] ++ emqx_connector_schema_lib:relational_db_fields()
|
] ++ emqx_connector_schema_lib:relational_db_fields()
|
||||||
++ emqx_connector_schema_lib:ssl_fields();
|
++ emqx_connector_schema_lib:ssl_fields().
|
||||||
|
|
||||||
fields(bcrypt) ->
|
password_hash_algorithm(type) -> {enum, [plain, md5, sha, sha256, sha512, bcrypt]};
|
||||||
[ {name, {enum, [bcrypt]}}
|
password_hash_algorithm(default) -> sha256;
|
||||||
, {salt_rounds, fun salt_rounds/1}
|
|
||||||
];
|
|
||||||
|
|
||||||
fields(other_algorithms) ->
|
|
||||||
[ {name, {enum, [plain, md5, sha, sha256, sha512]}}
|
|
||||||
].
|
|
||||||
|
|
||||||
password_hash_algorithm(type) -> {union, [hoconsc:ref(bcrypt), hoconsc:ref(other_algorithms)]};
|
|
||||||
password_hash_algorithm(default) -> #{<<"name">> => sha256};
|
|
||||||
password_hash_algorithm(_) -> undefined.
|
password_hash_algorithm(_) -> undefined.
|
||||||
|
|
||||||
salt_rounds(type) -> integer();
|
|
||||||
salt_rounds(default) -> 10;
|
|
||||||
salt_rounds(_) -> undefined.
|
|
||||||
|
|
||||||
salt_position(type) -> {enum, [prefix, suffix]};
|
salt_position(type) -> {enum, [prefix, suffix]};
|
||||||
salt_position(default) -> prefix;
|
salt_position(default) -> prefix;
|
||||||
salt_position(_) -> undefined.
|
salt_position(_) -> undefined.
|
||||||
|
@ -92,12 +80,13 @@ create(#{ password_hash_algorithm := Algorithm
|
||||||
salt_position => SaltPosition,
|
salt_position => SaltPosition,
|
||||||
query => Query,
|
query => Query,
|
||||||
placeholders => PlaceHolders,
|
placeholders => PlaceHolders,
|
||||||
query_timeout => QueryTimeout},
|
query_timeout => QueryTimeout,
|
||||||
|
'_unique' => Unique},
|
||||||
case emqx_resource:create_local(Unique, emqx_connector_mysql, Config) of
|
case emqx_resource:create_local(Unique, emqx_connector_mysql, Config) of
|
||||||
{ok, _} ->
|
{ok, _} ->
|
||||||
{ok, State#{resource_id => Unique}};
|
{ok, State};
|
||||||
{error, already_created} ->
|
{error, already_created} ->
|
||||||
{ok, State#{resource_id => Unique}};
|
{ok, State};
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
{error, Reason}
|
{error, Reason}
|
||||||
end.
|
end.
|
||||||
|
@ -114,12 +103,13 @@ update(Config, State) ->
|
||||||
authenticate(#{auth_method := _}, _) ->
|
authenticate(#{auth_method := _}, _) ->
|
||||||
ignore;
|
ignore;
|
||||||
authenticate(#{password := Password} = Credential,
|
authenticate(#{password := Password} = Credential,
|
||||||
#{resource_id := ResourceID,
|
#{placeholders := PlaceHolders,
|
||||||
placeholders := PlaceHolders,
|
|
||||||
query := Query,
|
query := Query,
|
||||||
query_timeout := Timeout} = State) ->
|
query_timeout := Timeout,
|
||||||
Params = emqx_authn_utils:replace_placeholder(PlaceHolders, Credential),
|
'_unique' := Unique} = State) ->
|
||||||
case emqx_resource:query(ResourceID, {sql, Query, Params, Timeout}) of
|
try
|
||||||
|
Params = emqx_authn_utils:replace_placeholders(PlaceHolders, Credential),
|
||||||
|
case emqx_resource:query(Unique, {sql, Query, Params, Timeout}) of
|
||||||
{ok, _Columns, []} -> ignore;
|
{ok, _Columns, []} -> ignore;
|
||||||
{ok, Columns, Rows} ->
|
{ok, Columns, Rows} ->
|
||||||
%% TODO: Support superuser
|
%% TODO: Support superuser
|
||||||
|
@ -127,23 +117,27 @@ authenticate(#{password := Password} = Credential,
|
||||||
check_password(Password, Selected, State);
|
check_password(Password, Selected, State);
|
||||||
{error, _Reason} ->
|
{error, _Reason} ->
|
||||||
ignore
|
ignore
|
||||||
|
end
|
||||||
|
catch
|
||||||
|
error:Reason ->
|
||||||
|
?LOG(warning, "The following error occurred in '~s' during authentication: ~p", [Unique, Reason]),
|
||||||
|
ignore
|
||||||
end.
|
end.
|
||||||
|
|
||||||
destroy(#{resource_id := ResourceID}) ->
|
destroy(#{'_unique' := Unique}) ->
|
||||||
_ = emqx_resource:remove_local(ResourceID),
|
_ = emqx_resource:remove_local(Unique),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
%% Internal functions
|
%% Internal functions
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
check_password(undefined, _Algorithm, _Selected) ->
|
check_password(undefined, _Selected, _State) ->
|
||||||
{error, bad_username_or_password};
|
{error, bad_username_or_password};
|
||||||
check_password(Password,
|
check_password(Password,
|
||||||
#{password_hash := Hash},
|
#{password_hash := Hash},
|
||||||
#{password_hash_algorithm := bcrypt}) ->
|
#{password_hash_algorithm := bcrypt}) ->
|
||||||
{ok, Hash0} = bcrypt:hashpw(Password, Hash),
|
case {ok, Hash} =:= bcrypt:hashpw(Password, Hash) of
|
||||||
case list_to_binary(Hash0) =:= Hash of
|
|
||||||
true -> ok;
|
true -> ok;
|
||||||
false -> {error, bad_username_or_password}
|
false -> {error, bad_username_or_password}
|
||||||
end;
|
end;
|
||||||
|
@ -163,7 +157,7 @@ check_password(Password,
|
||||||
|
|
||||||
%% TODO: Support prepare
|
%% TODO: Support prepare
|
||||||
parse_query(Query) ->
|
parse_query(Query) ->
|
||||||
case re:run(Query, "\\$\\{[a-z0-9\\_]+\\}", [global, {capture, all, binary}]) of
|
case re:run(Query, ?RE_PLACEHOLDER, [global, {capture, all, binary}]) of
|
||||||
{match, Captured} ->
|
{match, Captured} ->
|
||||||
PlaceHolders = [PlaceHolder || PlaceHolder <- Captured],
|
PlaceHolders = [PlaceHolder || PlaceHolder <- Captured],
|
||||||
NQuery = re:replace(Query, "'\\$\\{[a-z0-9\\_]+\\}'", "?", [global, {return, binary}]),
|
NQuery = re:replace(Query, "'\\$\\{[a-z0-9\\_]+\\}'", "?", [global, {return, binary}]),
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
-module(emqx_authn_pgsql).
|
-module(emqx_authn_pgsql).
|
||||||
|
|
||||||
-include("emqx_authn.hrl").
|
-include("emqx_authn.hrl").
|
||||||
|
-include_lib("emqx/include/logger.hrl").
|
||||||
-include_lib("typerefl/include/types.hrl").
|
-include_lib("typerefl/include/types.hrl").
|
||||||
|
|
||||||
-behaviour(hocon_schema).
|
-behaviour(hocon_schema).
|
||||||
|
@ -45,7 +46,8 @@ fields(config) ->
|
||||||
] ++ emqx_connector_schema_lib:relational_db_fields()
|
] ++ emqx_connector_schema_lib:relational_db_fields()
|
||||||
++ emqx_connector_schema_lib:ssl_fields().
|
++ emqx_connector_schema_lib:ssl_fields().
|
||||||
|
|
||||||
password_hash_algorithm(type) -> string();
|
password_hash_algorithm(type) -> {enum, [plain, md5, sha, sha256, sha512, bcrypt]};
|
||||||
|
password_hash_algorithm(default) -> sha256;
|
||||||
password_hash_algorithm(_) -> undefined.
|
password_hash_algorithm(_) -> undefined.
|
||||||
|
|
||||||
query(type) -> string();
|
query(type) -> string();
|
||||||
|
@ -65,12 +67,13 @@ create(#{ query := Query0
|
||||||
State = #{query => Query,
|
State = #{query => Query,
|
||||||
placeholders => PlaceHolders,
|
placeholders => PlaceHolders,
|
||||||
password_hash_algorithm => Algorithm,
|
password_hash_algorithm => Algorithm,
|
||||||
salt_position => SaltPosition},
|
salt_position => SaltPosition,
|
||||||
|
'_unique' => Unique},
|
||||||
case emqx_resource:create_local(Unique, emqx_connector_pgsql, Config) of
|
case emqx_resource:create_local(Unique, emqx_connector_pgsql, Config) of
|
||||||
{ok, _} ->
|
{ok, _} ->
|
||||||
{ok, State#{resource_id => Unique}};
|
{ok, State};
|
||||||
{error, already_created} ->
|
{error, already_created} ->
|
||||||
{ok, State#{resource_id => Unique}};
|
{ok, State};
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
{error, Reason}
|
{error, Reason}
|
||||||
end.
|
end.
|
||||||
|
@ -87,11 +90,12 @@ update(Config, State) ->
|
||||||
authenticate(#{auth_method := _}, _) ->
|
authenticate(#{auth_method := _}, _) ->
|
||||||
ignore;
|
ignore;
|
||||||
authenticate(#{password := Password} = Credential,
|
authenticate(#{password := Password} = Credential,
|
||||||
#{resource_id := ResourceID,
|
#{query := Query,
|
||||||
query := Query,
|
placeholders := PlaceHolders,
|
||||||
placeholders := PlaceHolders} = State) ->
|
'_unique' := Unique} = State) ->
|
||||||
Params = emqx_authn_utils:replace_placeholder(PlaceHolders, Credential),
|
try
|
||||||
case emqx_resource:query(ResourceID, {sql, Query, Params}) of
|
Params = emqx_authn_utils:replace_placeholders(PlaceHolders, Credential),
|
||||||
|
case emqx_resource:query(Unique, {sql, Query, Params}) of
|
||||||
{ok, _Columns, []} -> ignore;
|
{ok, _Columns, []} -> ignore;
|
||||||
{ok, Columns, Rows} ->
|
{ok, Columns, Rows} ->
|
||||||
%% TODO: Support superuser
|
%% TODO: Support superuser
|
||||||
|
@ -99,23 +103,27 @@ authenticate(#{password := Password} = Credential,
|
||||||
check_password(Password, Selected, State);
|
check_password(Password, Selected, State);
|
||||||
{error, _Reason} ->
|
{error, _Reason} ->
|
||||||
ignore
|
ignore
|
||||||
|
end
|
||||||
|
catch
|
||||||
|
error:Reason ->
|
||||||
|
?LOG(warning, "The following error occurred in '~s' during authentication: ~p", [Unique, Reason]),
|
||||||
|
ignore
|
||||||
end.
|
end.
|
||||||
|
|
||||||
destroy(#{resource_id := ResourceID}) ->
|
destroy(#{'_unique' := Unique}) ->
|
||||||
_ = emqx_resource:remove_local(ResourceID),
|
_ = emqx_resource:remove_local(Unique),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
%% Internal functions
|
%% Internal functions
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
check_password(undefined, _Algorithm, _Selected) ->
|
check_password(undefined, _Selected, _State) ->
|
||||||
{error, bad_username_or_password};
|
{error, bad_username_or_password};
|
||||||
check_password(Password,
|
check_password(Password,
|
||||||
#{password_hash := Hash},
|
#{password_hash := Hash},
|
||||||
#{password_hash_algorithm := bcrypt}) ->
|
#{password_hash_algorithm := bcrypt}) ->
|
||||||
{ok, Hash0} = bcrypt:hashpw(Password, Hash),
|
case {ok, Hash} =:= bcrypt:hashpw(Password, Hash) of
|
||||||
case list_to_binary(Hash0) =:= Hash of
|
|
||||||
true -> ok;
|
true -> ok;
|
||||||
false -> {error, bad_username_or_password}
|
false -> {error, bad_username_or_password}
|
||||||
end;
|
end;
|
||||||
|
@ -135,7 +143,7 @@ check_password(Password,
|
||||||
|
|
||||||
%% TODO: Support prepare
|
%% TODO: Support prepare
|
||||||
parse_query(Query) ->
|
parse_query(Query) ->
|
||||||
case re:run(Query, "\\$\\{[a-z0-9\\_]+\\}", [global, {capture, all, binary}]) of
|
case re:run(Query, ?RE_PLACEHOLDER, [global, {capture, all, binary}]) of
|
||||||
{match, Captured} ->
|
{match, Captured} ->
|
||||||
PlaceHolders = [PlaceHolder || PlaceHolder <- Captured],
|
PlaceHolders = [PlaceHolder || PlaceHolder <- Captured],
|
||||||
Replacements = ["$" ++ integer_to_list(I) || I <- lists:seq(1, length(Captured))],
|
Replacements = ["$" ++ integer_to_list(I) || I <- lists:seq(1, length(Captured))],
|
||||||
|
|
|
@ -149,11 +149,14 @@ connect(Opts) ->
|
||||||
WorkerOptions = proplists:get_value(worker_options, Opts, []),
|
WorkerOptions = proplists:get_value(worker_options, Opts, []),
|
||||||
mongo_api:connect(Type, Hosts, Options, WorkerOptions).
|
mongo_api:connect(Type, Hosts, Options, WorkerOptions).
|
||||||
|
|
||||||
mongo_query(Conn, find, Collection, Selector, Docs) ->
|
mongo_query(Conn, find, Collection, Selector, Projector) ->
|
||||||
mongo_api:find(Conn, Collection, Selector, Docs);
|
mongo_api:find(Conn, Collection, Selector, Projector);
|
||||||
|
|
||||||
|
mongo_query(Conn, find_one, Collection, Selector, Projector) ->
|
||||||
|
mongo_api:find_one(Conn, Collection, Selector, Projector);
|
||||||
|
|
||||||
%% Todo xxx
|
%% Todo xxx
|
||||||
mongo_query(_Conn, _Action, _Collection, _Selector, _Docs) ->
|
mongo_query(_Conn, _Action, _Collection, _Selector, _Projector) ->
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
do_start(InstId, Opts0, Config = #{mongo_type := Type,
|
do_start(InstId, Opts0, Config = #{mongo_type := Type,
|
||||||
|
|
|
@ -71,7 +71,7 @@ ssl_fields() ->
|
||||||
[ hoconsc:ref(?MODULE, ssl_on)
|
[ hoconsc:ref(?MODULE, ssl_on)
|
||||||
, hoconsc:ref(?MODULE, ssl_off)
|
, hoconsc:ref(?MODULE, ssl_off)
|
||||||
]),
|
]),
|
||||||
default => hoconsc:ref(?MODULE, ssl_off)
|
default => #{<<"enable">> => false}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
].
|
].
|
||||||
|
|
Loading…
Reference in New Issue