feat(mongo srv): support srv for mongodb authentication
This commit is contained in:
parent
d305111929
commit
25f504c90a
|
@ -5,7 +5,13 @@
|
||||||
## MongoDB Topology Type.
|
## MongoDB Topology Type.
|
||||||
##
|
##
|
||||||
## Value: single | unknown | sharded | rs
|
## Value: single | unknown | sharded | rs
|
||||||
auth.mongo.type = single
|
auth.mongo.type =
|
||||||
|
|
||||||
|
## Whether to use SRV and TXT records.
|
||||||
|
##
|
||||||
|
## Value: true | false
|
||||||
|
## Default: false
|
||||||
|
auth.mongo.srv_record = false
|
||||||
|
|
||||||
## The set name if type is rs.
|
## The set name if type is rs.
|
||||||
##
|
##
|
||||||
|
@ -37,7 +43,6 @@ auth.mongo.pool = 8
|
||||||
## MongoDB AuthSource
|
## MongoDB AuthSource
|
||||||
##
|
##
|
||||||
## Value: String
|
## Value: String
|
||||||
## Default: mqtt
|
|
||||||
## auth.mongo.auth_source = admin
|
## auth.mongo.auth_source = admin
|
||||||
|
|
||||||
## MongoDB database
|
## MongoDB database
|
||||||
|
|
|
@ -6,8 +6,12 @@
|
||||||
{datatype, {enum, [single, unknown, sharded, rs]}}
|
{datatype, {enum, [single, unknown, sharded, rs]}}
|
||||||
]}.
|
]}.
|
||||||
|
|
||||||
|
{mapping, "auth.mongo.srv_record", "emqx_auth_mongo.server", [
|
||||||
|
{default, false},
|
||||||
|
{datatype, {enum, [true, false]}}
|
||||||
|
]}.
|
||||||
|
|
||||||
{mapping, "auth.mongo.rs_set_name", "emqx_auth_mongo.server", [
|
{mapping, "auth.mongo.rs_set_name", "emqx_auth_mongo.server", [
|
||||||
{default, "mqtt"},
|
|
||||||
{datatype, string}
|
{datatype, string}
|
||||||
]}.
|
]}.
|
||||||
|
|
||||||
|
@ -41,7 +45,6 @@
|
||||||
]}.
|
]}.
|
||||||
|
|
||||||
{mapping, "auth.mongo.auth_source", "emqx_auth_mongo.server", [
|
{mapping, "auth.mongo.auth_source", "emqx_auth_mongo.server", [
|
||||||
{default, "mqtt"},
|
|
||||||
{datatype, string}
|
{datatype, string}
|
||||||
]}.
|
]}.
|
||||||
|
|
||||||
|
@ -101,9 +104,9 @@
|
||||||
]}.
|
]}.
|
||||||
|
|
||||||
{translation, "emqx_auth_mongo.server", fun(Conf) ->
|
{translation, "emqx_auth_mongo.server", fun(Conf) ->
|
||||||
H = cuttlefish:conf_get("auth.mongo.server", Conf),
|
SrvRecord = cuttlefish:conf_get("auth.mongo.srv_record", Conf, false),
|
||||||
Hosts = string:tokens(H, ","),
|
Server = cuttlefish:conf_get("auth.mongo.server", Conf),
|
||||||
Type0 = cuttlefish:conf_get("auth.mongo.type", Conf),
|
Type = cuttlefish:conf_get("auth.mongo.type", Conf),
|
||||||
Pool = cuttlefish:conf_get("auth.mongo.pool", Conf),
|
Pool = cuttlefish:conf_get("auth.mongo.pool", Conf),
|
||||||
%% FIXME: compatible with 4.0-4.2 version format, plan to delete in 5.0
|
%% FIXME: compatible with 4.0-4.2 version format, plan to delete in 5.0
|
||||||
Login = cuttlefish:conf_get("auth.mongo.username", Conf,
|
Login = cuttlefish:conf_get("auth.mongo.username", Conf,
|
||||||
|
@ -111,7 +114,10 @@
|
||||||
),
|
),
|
||||||
Passwd = cuttlefish:conf_get("auth.mongo.password", Conf),
|
Passwd = cuttlefish:conf_get("auth.mongo.password", Conf),
|
||||||
DB = cuttlefish:conf_get("auth.mongo.database", Conf),
|
DB = cuttlefish:conf_get("auth.mongo.database", Conf),
|
||||||
AuthSrc = cuttlefish:conf_get("auth.mongo.auth_source", Conf),
|
AuthSource = case cuttlefish:conf_get("auth.mongo.auth_source", Conf, undefined) of
|
||||||
|
undefined -> [];
|
||||||
|
AuthSource0 -> [{auth_source, list_to_binary(AuthSource0)}]
|
||||||
|
end,
|
||||||
R = cuttlefish:conf_get("auth.mongo.w_mode", Conf),
|
R = cuttlefish:conf_get("auth.mongo.w_mode", Conf),
|
||||||
W = cuttlefish:conf_get("auth.mongo.r_mode", Conf),
|
W = cuttlefish:conf_get("auth.mongo.r_mode", Conf),
|
||||||
Login0 = case Login =:= [] of
|
Login0 = case Login =:= [] of
|
||||||
|
@ -156,8 +162,8 @@
|
||||||
false -> []
|
false -> []
|
||||||
end,
|
end,
|
||||||
|
|
||||||
WorkerOptions = [{database, list_to_binary(DB)}, {auth_source, list_to_binary(AuthSrc)}]
|
WorkerOptions = [{database, list_to_binary(DB)}]
|
||||||
++ Login0 ++ Passwd0 ++ W0 ++ R0 ++ Ssl,
|
++ Login0 ++ Passwd0 ++ W0 ++ R0 ++ Ssl ++ AuthSource,
|
||||||
|
|
||||||
Vars = cuttlefish_variable:fuzzy_matches(["auth", "mongo", "topology", "$name"], Conf),
|
Vars = cuttlefish_variable:fuzzy_matches(["auth", "mongo", "topology", "$name"], Conf),
|
||||||
Options = lists:map(fun({_, Name}) ->
|
Options = lists:map(fun({_, Name}) ->
|
||||||
|
@ -174,16 +180,17 @@
|
||||||
{list_to_atom(Name2), cuttlefish:conf_get("auth.mongo.topology."++Name, Conf)}
|
{list_to_atom(Name2), cuttlefish:conf_get("auth.mongo.topology."++Name, Conf)}
|
||||||
end, Vars),
|
end, Vars),
|
||||||
|
|
||||||
Type = case Type0 =:= rs of
|
ReplicaSet = case cuttlefish:conf_get("auth.mongo.rs_set_name", Conf, undefined) of
|
||||||
true -> {Type0, list_to_binary(cuttlefish:conf_get("auth.mongo.rs_set_name", Conf))};
|
undefined -> [];
|
||||||
false -> Type0
|
ReplicaSet0 -> [{rs_set_name, list_to_binary(ReplicaSet0)}]
|
||||||
end,
|
end,
|
||||||
[{type, Type},
|
[{srv_record, SrvRecord},
|
||||||
{hosts, Hosts},
|
{type, Type},
|
||||||
|
{server, Server},
|
||||||
{options, Options},
|
{options, Options},
|
||||||
{worker_options, WorkerOptions},
|
{worker_options, WorkerOptions},
|
||||||
{auto_reconnect, 1},
|
{auto_reconnect, 1},
|
||||||
{pool_size, Pool}]
|
{pool_size, Pool}] ++ ReplicaSet
|
||||||
end}.
|
end}.
|
||||||
|
|
||||||
%% The mongodb operation timeout is specified by the value of `cursor_timeout` from application config,
|
%% The mongodb operation timeout is specified by the value of `cursor_timeout` from application config,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{application, emqx_auth_mongo,
|
{application, emqx_auth_mongo,
|
||||||
[{description, "EMQ X Authentication/ACL with MongoDB"},
|
[{description, "EMQ X Authentication/ACL with MongoDB"},
|
||||||
{vsn, "4.3.0"}, % strict semver, bump manually!
|
{vsn, "4.4.0"}, % strict semver, bump manually!
|
||||||
{modules, []},
|
{modules, []},
|
||||||
{registered, [emqx_auth_mongo_sup]},
|
{registered, [emqx_auth_mongo_sup]},
|
||||||
{applications, [kernel,stdlib,mongodb,ecpool]},
|
{applications, [kernel,stdlib,mongodb,ecpool]},
|
||||||
|
|
|
@ -28,7 +28,96 @@ start_link() ->
|
||||||
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
|
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
|
||||||
|
|
||||||
init([]) ->
|
init([]) ->
|
||||||
{ok, PoolEnv} = application:get_env(?APP, server),
|
{ok, Opts} = application:get_env(?APP, server),
|
||||||
PoolSpec = ecpool:pool_spec(?APP, ?APP, ?APP, PoolEnv),
|
NOpts = may_parse_srv_and_txt_records(Opts),
|
||||||
|
PoolSpec = ecpool:pool_spec(?APP, ?APP, ?APP, NOpts),
|
||||||
{ok, {{one_for_all, 10, 100}, [PoolSpec]}}.
|
{ok, {{one_for_all, 10, 100}, [PoolSpec]}}.
|
||||||
|
|
||||||
|
may_parse_srv_and_txt_records(Opts) when is_list(Opts) ->
|
||||||
|
maps:to_list(may_parse_srv_and_txt_records(maps:from_list(Opts)));
|
||||||
|
|
||||||
|
may_parse_srv_and_txt_records(#{type := Type,
|
||||||
|
srv_record := false,
|
||||||
|
server := Server} = Opts) ->
|
||||||
|
Hosts = to_hosts(Server),
|
||||||
|
case Type =:= rs of
|
||||||
|
true ->
|
||||||
|
case maps:get(rs_set_name, Opts, undefined) of
|
||||||
|
undefined ->
|
||||||
|
error({missing_parameter, rs_set_name});
|
||||||
|
ReplicaSet ->
|
||||||
|
Opts#{type => {rs, ReplicaSet},
|
||||||
|
hosts => Hosts}
|
||||||
|
end;
|
||||||
|
false ->
|
||||||
|
Opts#{hosts => Hosts}
|
||||||
|
end;
|
||||||
|
|
||||||
|
may_parse_srv_and_txt_records(#{type := Type,
|
||||||
|
srv_record := true,
|
||||||
|
server := Server,
|
||||||
|
worker_options := WorkerOptions} = Opts) ->
|
||||||
|
Hosts = parse_srv_records(Server),
|
||||||
|
Opts0 = parse_txt_records(Type, Server),
|
||||||
|
NWorkerOptions = maps:to_list(maps:merge(maps:from_list(WorkerOptions), maps:with([auth_source], Opts0))),
|
||||||
|
NOpts = Opts#{hosts => Hosts, worker_options => NWorkerOptions},
|
||||||
|
case Type =:= rs of
|
||||||
|
true ->
|
||||||
|
case maps:get(rs_set_name, Opts0, maps:get(rs_set_name, NOpts, undefined)) of
|
||||||
|
undefined ->
|
||||||
|
error({missing_parameter, rs_set_name});
|
||||||
|
ReplicaSet ->
|
||||||
|
NOpts#{type => {Type, ReplicaSet}}
|
||||||
|
end;
|
||||||
|
false ->
|
||||||
|
NOpts
|
||||||
|
end.
|
||||||
|
|
||||||
|
to_hosts(Server) ->
|
||||||
|
[string:trim(H) || H <- string:tokens(Server, ",")].
|
||||||
|
|
||||||
|
parse_srv_records(Server) ->
|
||||||
|
case inet_res:lookup("_mongodb._tcp." ++ Server, in, srv) of
|
||||||
|
[] ->
|
||||||
|
error(service_not_found);
|
||||||
|
Services ->
|
||||||
|
[Host ++ ":" ++ integer_to_list(Port) || {_, _, Port, Host} <- Services]
|
||||||
|
end.
|
||||||
|
|
||||||
|
parse_txt_records(Type, Server) ->
|
||||||
|
case inet_res:lookup(Server, in, txt) of
|
||||||
|
[] ->
|
||||||
|
#{};
|
||||||
|
[[QueryString]] ->
|
||||||
|
case uri_string:dissect_query(QueryString) of
|
||||||
|
{error, _, _} ->
|
||||||
|
error({invalid_txt_record, invalid_query_string});
|
||||||
|
Options ->
|
||||||
|
Fields = case Type of
|
||||||
|
rs -> ["authSource", "replicaSet"];
|
||||||
|
_ -> ["authSource"]
|
||||||
|
end,
|
||||||
|
take_and_convert(Fields, Options)
|
||||||
|
end;
|
||||||
|
_ ->
|
||||||
|
error({invalid_txt_record, multiple_records})
|
||||||
|
end.
|
||||||
|
|
||||||
|
take_and_convert(Fields, Options) ->
|
||||||
|
take_and_convert(Fields, Options, #{}).
|
||||||
|
|
||||||
|
take_and_convert([], [_ | _], _Acc) ->
|
||||||
|
error({invalid_txt_record, invalid_option});
|
||||||
|
take_and_convert([], [], Acc) ->
|
||||||
|
Acc;
|
||||||
|
take_and_convert([Field | More], Options, Acc) ->
|
||||||
|
case lists:keytake(Field, 1, Options) of
|
||||||
|
{value, {"authSource", V}, NOptions} ->
|
||||||
|
take_and_convert(More, NOptions, Acc#{auth_source => list_to_binary(V)});
|
||||||
|
{value, {"replicaSet", V}, NOptions} ->
|
||||||
|
take_and_convert(More, NOptions, Acc#{rs_set_name => list_to_binary(V)});
|
||||||
|
{value, _, _} ->
|
||||||
|
error({invalid_txt_record, invalid_option});
|
||||||
|
false ->
|
||||||
|
take_and_convert(More, Options, Acc)
|
||||||
|
end.
|
||||||
|
|
Loading…
Reference in New Issue