feat(one authn): merge simple authn and enhanced authn
This commit is contained in:
parent
fe779925c5
commit
6a8e35ce3a
|
@ -18,27 +18,22 @@
|
|||
|
||||
-include("emqx.hrl").
|
||||
|
||||
-export([authenticate/1]).
|
||||
|
||||
-export([ authorize/3
|
||||
-export([ authenticate/1
|
||||
, authorize/3
|
||||
]).
|
||||
|
||||
-type(result() :: #{auth_result := emqx_types:auth_result(),
|
||||
anonymous := boolean()
|
||||
}).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% APIs
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-spec(authenticate(emqx_types:clientinfo()) -> {ok, result()} | {error, term()}).
|
||||
authenticate(ClientInfo = #{zone := Zone}) ->
|
||||
AuthResult = default_auth_result(Zone),
|
||||
case emqx_zone:get_env(Zone, bypass_auth_plugins, false) of
|
||||
-spec(authenticate(emqx_types:clientinfo()) ->
|
||||
ok | {ok, binary()} | {continue, map()} | {continue, binary(), map()} | {error, term()}).
|
||||
authenticate(Credential = #{zone := Zone}) ->
|
||||
case emqx_zone:get_env(Zone, bypass_authentication, false) of
|
||||
true ->
|
||||
return_auth_result(AuthResult);
|
||||
ok;
|
||||
false ->
|
||||
return_auth_result(run_hooks('client.authenticate', [ClientInfo], AuthResult))
|
||||
run_hooks('client.authenticate', [Credential], ok)
|
||||
end.
|
||||
|
||||
%% @doc Check ACL
|
||||
|
@ -65,18 +60,6 @@ do_authorize(ClientInfo, PubSub, Topic) ->
|
|||
_Other -> deny
|
||||
end.
|
||||
|
||||
default_auth_result(Zone) ->
|
||||
case emqx_zone:get_env(Zone, allow_anonymous, false) of
|
||||
true -> #{auth_result => success, anonymous => true};
|
||||
false -> #{auth_result => not_authorized, anonymous => false}
|
||||
end.
|
||||
|
||||
-compile({inline, [run_hooks/3]}).
|
||||
run_hooks(Name, Args, Acc) ->
|
||||
ok = emqx_metrics:inc(Name), emqx_hooks:run_fold(Name, Args, Acc).
|
||||
|
||||
-compile({inline, [return_auth_result/1]}).
|
||||
return_auth_result(Result = #{auth_result := success}) ->
|
||||
{ok, Result};
|
||||
return_auth_result(Result) ->
|
||||
{error, maps:get(auth_result, Result, unknown_error)}.
|
||||
|
|
|
@ -98,7 +98,7 @@
|
|||
|
||||
-type(channel() :: #channel{}).
|
||||
|
||||
-type(conn_state() :: idle | connecting | connected | disconnected).
|
||||
-type(conn_state() :: idle | connecting | connected | reauthenticating | disconnected).
|
||||
|
||||
-type(reply() :: {outgoing, emqx_types:packet()}
|
||||
| {outgoing, [emqx_types:packet()]}
|
||||
|
@ -272,7 +272,8 @@ take_ws_cookie(ClientInfo, ConnInfo) ->
|
|||
| {ok, replies(), channel()}
|
||||
| {shutdown, Reason :: term(), channel()}
|
||||
| {shutdown, Reason :: term(), replies(), channel()}).
|
||||
handle_in(?CONNECT_PACKET(), Channel = #channel{conn_state = connected}) ->
|
||||
handle_in(?CONNECT_PACKET(), Channel = #channel{conn_state = ConnState})
|
||||
when ConnState =:= connected orelse ConnState =:= reauthenticating ->
|
||||
handle_out(disconnect, ?RC_PROTOCOL_ERROR, Channel);
|
||||
|
||||
handle_in(?CONNECT_PACKET(ConnPkt), Channel) ->
|
||||
|
@ -281,56 +282,64 @@ handle_in(?CONNECT_PACKET(ConnPkt), Channel) ->
|
|||
fun check_connect/2,
|
||||
fun enrich_client/2,
|
||||
fun set_log_meta/2,
|
||||
fun check_banned/2,
|
||||
fun auth_connect/2
|
||||
fun check_banned/2
|
||||
], ConnPkt, Channel#channel{conn_state = connecting}) of
|
||||
{ok, NConnPkt, NChannel = #channel{clientinfo = ClientInfo}} ->
|
||||
NChannel1 = NChannel#channel{
|
||||
will_msg = emqx_packet:will_msg(NConnPkt),
|
||||
alias_maximum = init_alias_maximum(NConnPkt, ClientInfo)
|
||||
},
|
||||
case enhanced_auth(?CONNECT_PACKET(NConnPkt), NChannel1) of
|
||||
case authenticate(?CONNECT_PACKET(NConnPkt), NChannel1) of
|
||||
{ok, Properties, NChannel2} ->
|
||||
process_connect(Properties, ensure_connected(NChannel2));
|
||||
{continue, Properties, NChannel2} ->
|
||||
handle_out(auth, {?RC_CONTINUE_AUTHENTICATION, Properties}, NChannel2);
|
||||
{error, ReasonCode, NChannel2} ->
|
||||
handle_out(connack, ReasonCode, NChannel2)
|
||||
{error, ReasonCode} ->
|
||||
handle_out(connack, ReasonCode, NChannel1)
|
||||
end;
|
||||
{error, ReasonCode, NChannel} ->
|
||||
handle_out(connack, ReasonCode, NChannel)
|
||||
end;
|
||||
|
||||
handle_in(Packet = ?AUTH_PACKET(?RC_CONTINUE_AUTHENTICATION, _Properties),
|
||||
handle_in(Packet = ?AUTH_PACKET(ReasonCode, _Properties),
|
||||
Channel = #channel{conn_state = ConnState}) ->
|
||||
case enhanced_auth(Packet, Channel) of
|
||||
{ok, NProperties, NChannel} ->
|
||||
try
|
||||
case {ReasonCode, ConnState} of
|
||||
{?RC_CONTINUE_AUTHENTICATION, connecting} -> ok;
|
||||
{?RC_CONTINUE_AUTHENTICATION, reauthenticating} -> ok;
|
||||
{?RC_RE_AUTHENTICATE, connected} -> ok;
|
||||
_ -> error(protocol_error)
|
||||
end,
|
||||
case authenticate(Packet, Channel) of
|
||||
{ok, NProperties, NChannel} ->
|
||||
case ConnState of
|
||||
connecting ->
|
||||
process_connect(NProperties, ensure_connected(NChannel));
|
||||
_ ->
|
||||
handle_out(auth, {?RC_SUCCESS, NProperties}, NChannel#channel{conn_state = connected})
|
||||
end;
|
||||
{continue, NProperties, NChannel} ->
|
||||
handle_out(auth, {?RC_CONTINUE_AUTHENTICATION, NProperties}, NChannel#channel{conn_state = reauthenticating});
|
||||
{error, NReasonCode} ->
|
||||
case ConnState of
|
||||
connecting ->
|
||||
handle_out(connack, NReasonCode, Channel);
|
||||
_ ->
|
||||
handle_out(disconnect, NReasonCode, Channel)
|
||||
end
|
||||
end
|
||||
catch
|
||||
_Class:_Reason ->
|
||||
case ConnState of
|
||||
connecting ->
|
||||
process_connect(NProperties, ensure_connected(NChannel));
|
||||
connected ->
|
||||
handle_out(auth, {?RC_SUCCESS, NProperties}, NChannel);
|
||||
handle_out(connack, ?RC_PROTOCOL_ERROR, Channel);
|
||||
_ ->
|
||||
handle_out(disconnect, ?RC_PROTOCOL_ERROR, Channel)
|
||||
end;
|
||||
{continue, NProperties, NChannel} ->
|
||||
handle_out(auth, {?RC_CONTINUE_AUTHENTICATION, NProperties}, NChannel);
|
||||
{error, NReasonCode, NChannel} ->
|
||||
handle_out(connack, NReasonCode, NChannel)
|
||||
end
|
||||
end;
|
||||
|
||||
handle_in(Packet = ?AUTH_PACKET(?RC_RE_AUTHENTICATE, _Properties),
|
||||
Channel = #channel{conn_state = connected}) ->
|
||||
case enhanced_auth(Packet, Channel) of
|
||||
{ok, NProperties, NChannel} ->
|
||||
handle_out(auth, {?RC_SUCCESS, NProperties}, NChannel);
|
||||
{continue, NProperties, NChannel} ->
|
||||
handle_out(auth, {?RC_CONTINUE_AUTHENTICATION, NProperties}, NChannel);
|
||||
{error, NReasonCode, NChannel} ->
|
||||
handle_out(disconnect, NReasonCode, NChannel)
|
||||
end;
|
||||
|
||||
handle_in(?PACKET(_), Channel = #channel{conn_state = ConnState}) when ConnState =/= connected ->
|
||||
handle_in(?PACKET(_), Channel = #channel{conn_state = ConnState})
|
||||
when ConnState =/= connected andalso ConnState =/= reauthenticating ->
|
||||
handle_out(disconnect, ?RC_PROTOCOL_ERROR, Channel);
|
||||
|
||||
handle_in(Packet = ?PUBLISH_PACKET(_QoS), Channel) ->
|
||||
|
@ -469,9 +478,11 @@ handle_in({frame_error, frame_too_large}, Channel = #channel{conn_state = connec
|
|||
handle_in({frame_error, Reason}, Channel = #channel{conn_state = connecting}) ->
|
||||
shutdown(Reason, ?CONNACK_PACKET(?RC_MALFORMED_PACKET), Channel);
|
||||
|
||||
handle_in({frame_error, frame_too_large}, Channel = #channel{conn_state = connected}) ->
|
||||
handle_in({frame_error, frame_too_large}, Channel = #channel{conn_state = ConnState})
|
||||
when ConnState =:= connected orelse ConnState =:= reauthenticating ->
|
||||
handle_out(disconnect, {?RC_PACKET_TOO_LARGE, frame_too_large}, Channel);
|
||||
handle_in({frame_error, Reason}, Channel = #channel{conn_state = connected}) ->
|
||||
handle_in({frame_error, Reason}, Channel = #channel{conn_state = ConnState})
|
||||
when ConnState =:= connected orelse ConnState =:= reauthenticating ->
|
||||
handle_out(disconnect, {?RC_MALFORMED_PACKET, Reason}, Channel);
|
||||
|
||||
handle_in({frame_error, Reason}, Channel = #channel{conn_state = disconnected}) ->
|
||||
|
@ -967,8 +978,9 @@ handle_info({sock_closed, Reason}, Channel = #channel{conn_state = connecting})
|
|||
shutdown(Reason, Channel);
|
||||
|
||||
handle_info({sock_closed, Reason}, Channel =
|
||||
#channel{conn_state = connected,
|
||||
clientinfo = ClientInfo = #{zone := Zone}}) ->
|
||||
#channel{conn_state = ConnState,
|
||||
clientinfo = ClientInfo = #{zone := Zone}})
|
||||
when ConnState =:= connected orelse ConnState =:= reauthenticating ->
|
||||
emqx_zone:enable_flapping_detect(Zone)
|
||||
andalso emqx_flapping:detect(ClientInfo),
|
||||
Channel1 = ensure_disconnected(Reason, mabye_publish_will_msg(Channel)),
|
||||
|
@ -1241,75 +1253,60 @@ check_banned(_ConnPkt, #channel{clientinfo = ClientInfo = #{zone := Zone}}) ->
|
|||
end.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Auth Connect
|
||||
%% Authenticate
|
||||
|
||||
auth_connect(#mqtt_packet_connect{password = Password},
|
||||
authenticate(?CONNECT_PACKET(
|
||||
#mqtt_packet_connect{
|
||||
proto_ver = ?MQTT_PROTO_V5,
|
||||
properties = #{'Authentication-Method' := AuthMethod} = Properties}),
|
||||
#channel{clientinfo = ClientInfo,
|
||||
auth_cache = AuthCache} = Channel) ->
|
||||
AuthData = emqx_mqtt_props:get('Authentication-Data', Properties, undefined),
|
||||
do_authenticate(ClientInfo#{auth_method => AuthMethod,
|
||||
auth_data => AuthData,
|
||||
auth_cache => AuthCache}, Channel);
|
||||
|
||||
authenticate(?CONNECT_PACKET(#mqtt_packet_connect{password = Password}),
|
||||
#channel{clientinfo = ClientInfo} = Channel) ->
|
||||
#{clientid := ClientId,
|
||||
username := Username} = ClientInfo,
|
||||
case emqx_access_control:authenticate(ClientInfo#{password => Password}) of
|
||||
{ok, AuthResult} ->
|
||||
is_anonymous(AuthResult) andalso
|
||||
emqx_metrics:inc('client.auth.anonymous'),
|
||||
NClientInfo = maps:merge(ClientInfo, AuthResult),
|
||||
{ok, Channel#channel{clientinfo = NClientInfo}};
|
||||
{error, Reason} ->
|
||||
?LOG(warning, "Client ~s (Username: '~s') login failed for ~0p",
|
||||
[ClientId, Username, Reason]),
|
||||
{error, emqx_reason_codes:connack_error(Reason)}
|
||||
do_authenticate(ClientInfo#{password => Password}, Channel);
|
||||
|
||||
authenticate(?AUTH_PACKET(_, #{'Authentication-Method' := AuthMethod} = Properties),
|
||||
#channel{clientinfo = ClientInfo,
|
||||
conninfo = #{conn_props := ConnProps},
|
||||
auth_cache = AuthCache} = Channel) ->
|
||||
case emqx_mqtt_props:get('Authentication-Method', ConnProps, undefined) of
|
||||
AuthMethod ->
|
||||
AuthData = emqx_mqtt_props:get('Authentication-Data', Properties, undefined),
|
||||
do_authenticate(ClientInfo#{auth_method => AuthMethod,
|
||||
auth_data => AuthData,
|
||||
auth_cache => AuthCache}, Channel);
|
||||
_ ->
|
||||
{error, ?RC_BAD_AUTHENTICATION_METHOD}
|
||||
end.
|
||||
|
||||
is_anonymous(#{anonymous := true}) -> true;
|
||||
is_anonymous(_AuthResult) -> false.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Enhanced Authentication
|
||||
|
||||
enhanced_auth(?CONNECT_PACKET(#mqtt_packet_connect{
|
||||
proto_ver = Protover,
|
||||
properties = Properties
|
||||
}), Channel) ->
|
||||
case Protover of
|
||||
?MQTT_PROTO_V5 ->
|
||||
AuthMethod = emqx_mqtt_props:get('Authentication-Method', Properties, undefined),
|
||||
AuthData = emqx_mqtt_props:get('Authentication-Data', Properties, undefined),
|
||||
do_enhanced_auth(AuthMethod, AuthData, Channel);
|
||||
_ ->
|
||||
{ok, #{}, Channel}
|
||||
do_authenticate(#{auth_method := AuthMethod} = Credential, Channel) ->
|
||||
Properties = #{'Authentication-Method' => AuthMethod},
|
||||
case emqx_access_control:authenticate(Credential) of
|
||||
ok ->
|
||||
{ok, Properties, Channel#channel{auth_cache = #{}}};
|
||||
{ok, AuthData} ->
|
||||
{ok, Properties#{'Authentication-Data' => AuthData},
|
||||
Channel#channel{auth_cache = #{}}};
|
||||
{continue, AuthCache} ->
|
||||
{continue, Properties, Channel#channel{auth_cache = AuthCache}};
|
||||
{continue, AuthData, AuthCache} ->
|
||||
{continue, Properties#{'Authentication-Data' => AuthData},
|
||||
Channel#channel{auth_cache = AuthCache}};
|
||||
{error, Reason} ->
|
||||
{error, emqx_reason_codes:connack_error(Reason)}
|
||||
end;
|
||||
|
||||
enhanced_auth(?AUTH_PACKET(_ReasonCode, Properties), Channel = #channel{conninfo = ConnInfo}) ->
|
||||
AuthMethod = emqx_mqtt_props:get('Authentication-Method',
|
||||
emqx_mqtt_props:get(conn_props, ConnInfo, #{}),
|
||||
undefined
|
||||
),
|
||||
NAuthMethod = emqx_mqtt_props:get('Authentication-Method', Properties, undefined),
|
||||
AuthData = emqx_mqtt_props:get('Authentication-Data', Properties, undefined),
|
||||
case NAuthMethod =:= undefined orelse NAuthMethod =/= AuthMethod of
|
||||
true ->
|
||||
{error, emqx_reason_codes:connack_error(bad_authentication_method), Channel};
|
||||
false ->
|
||||
do_enhanced_auth(AuthMethod, AuthData, Channel)
|
||||
end.
|
||||
|
||||
do_enhanced_auth(undefined, undefined, Channel) ->
|
||||
{ok, #{}, Channel};
|
||||
do_enhanced_auth(undefined, _AuthData, Channel) ->
|
||||
{error, emqx_reason_codes:connack_error(not_authorized), Channel};
|
||||
do_enhanced_auth(_AuthMethod, undefined, Channel) ->
|
||||
{error, emqx_reason_codes:connack_error(not_authorized), Channel};
|
||||
do_enhanced_auth(AuthMethod, AuthData, Channel = #channel{auth_cache = Cache}) ->
|
||||
case run_hooks('client.enhanced_authenticate', [AuthMethod, AuthData], Cache) of
|
||||
{ok, NAuthData, NCache} ->
|
||||
NProperties = #{'Authentication-Method' => AuthMethod,
|
||||
'Authentication-Data' => NAuthData},
|
||||
{ok, NProperties, Channel#channel{auth_cache = NCache}};
|
||||
{continue, NAuthData, NCache} ->
|
||||
NProperties = #{'Authentication-Method' => AuthMethod,
|
||||
'Authentication-Data' => NAuthData},
|
||||
{continue, NProperties, Channel#channel{auth_cache = NCache}};
|
||||
_ ->
|
||||
{error, emqx_reason_codes:connack_error(not_authorized), Channel}
|
||||
do_authenticate(Credential, Channel) ->
|
||||
case emqx_access_control:authenticate(Credential) of
|
||||
ok ->
|
||||
{ok, #{}, Channel};
|
||||
{error, Reason} ->
|
||||
{error, emqx_reason_codes:connack_error(Reason)}
|
||||
end.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
|
@ -1703,7 +1700,8 @@ shutdown(Reason, Reply, Packet, Channel) ->
|
|||
{shutdown, Reason, Reply, Packet, Channel}.
|
||||
|
||||
disconnect_and_shutdown(Reason, Reply, Channel = ?IS_MQTT_V5
|
||||
= #channel{conn_state = connected}) ->
|
||||
= #channel{conn_state = ConnState})
|
||||
when ConnState =:= connected orelse ConnState =:= reauthenticating ->
|
||||
shutdown(Reason, Reply, ?DISCONNECT_PACKET(reason_code(Reason)), Channel);
|
||||
|
||||
disconnect_and_shutdown(Reason, Reply, Channel) ->
|
||||
|
|
|
@ -170,16 +170,11 @@ frame_error(frame_too_large) -> ?RC_PACKET_TOO_LARGE;
|
|||
frame_error(_) -> ?RC_MALFORMED_PACKET.
|
||||
|
||||
connack_error(protocol_error) -> ?RC_PROTOCOL_ERROR;
|
||||
connack_error(client_identifier_not_valid) -> ?RC_CLIENT_IDENTIFIER_NOT_VALID;
|
||||
connack_error(bad_username_or_password) -> ?RC_BAD_USER_NAME_OR_PASSWORD;
|
||||
connack_error(bad_clientid_or_password) -> ?RC_BAD_USER_NAME_OR_PASSWORD;
|
||||
connack_error(username_or_password_undefined) -> ?RC_BAD_USER_NAME_OR_PASSWORD;
|
||||
connack_error(password_error) -> ?RC_BAD_USER_NAME_OR_PASSWORD;
|
||||
connack_error(not_authorized) -> ?RC_NOT_AUTHORIZED;
|
||||
connack_error(server_unavailable) -> ?RC_SERVER_UNAVAILABLE;
|
||||
connack_error(server_busy) -> ?RC_SERVER_BUSY;
|
||||
connack_error(banned) -> ?RC_BANNED;
|
||||
connack_error(bad_authentication_method) -> ?RC_BAD_AUTHENTICATION_METHOD;
|
||||
%% TODO: ???
|
||||
connack_error(_) -> ?RC_NOT_AUTHORIZED.
|
||||
connack_error(_) -> ?RC_UNSPECIFIED_ERROR.
|
||||
|
||||
|
|
|
@ -1,26 +1,12 @@
|
|||
emqx_authn: {
|
||||
chains: [
|
||||
# {
|
||||
# id: "chain1"
|
||||
# type: simple
|
||||
# authenticators: [
|
||||
# {
|
||||
# name: "authenticator1"
|
||||
# type: built-in-database
|
||||
# config: {
|
||||
# user_id_type: clientid
|
||||
# password_hash_algorithm: {
|
||||
# name: sha256
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
# ]
|
||||
# }
|
||||
]
|
||||
bindings: [
|
||||
# {
|
||||
# chain_id: "chain1"
|
||||
# listeners: ["mqtt-tcp", "mqtt-ssl"]
|
||||
# }
|
||||
authenticators: [
|
||||
# {
|
||||
# name: "authenticator1"
|
||||
# mechanism: password-based
|
||||
# config: {
|
||||
# server_type: built-in-database
|
||||
# user_id_type: clientid
|
||||
# }
|
||||
# }
|
||||
]
|
||||
}
|
||||
|
|
|
@ -15,16 +15,15 @@
|
|||
%%--------------------------------------------------------------------
|
||||
|
||||
-define(APP, emqx_authn).
|
||||
-define(CHAIN, <<"mqtt">>).
|
||||
|
||||
-type chain_id() :: binary().
|
||||
-type authn_type() :: simple | enhanced.
|
||||
-type authenticator_name() :: binary().
|
||||
-type authenticator_type() :: mnesia | jwt | mysql | postgresql.
|
||||
-type listener_id() :: binary().
|
||||
-type mechanism() :: 'password-based' | jwt | scram.
|
||||
|
||||
-record(authenticator,
|
||||
{ name :: authenticator_name()
|
||||
, type :: authenticator_type()
|
||||
, mechanism :: mechanism()
|
||||
, provider :: module()
|
||||
, config :: map()
|
||||
, state :: map()
|
||||
|
@ -32,16 +31,10 @@
|
|||
|
||||
-record(chain,
|
||||
{ id :: chain_id()
|
||||
, type :: authn_type()
|
||||
, authenticators :: [{authenticator_name(), #authenticator{}}]
|
||||
, created_at :: integer()
|
||||
}).
|
||||
|
||||
-record(binding,
|
||||
{ bound :: {listener_id(), authn_type()}
|
||||
, chain_id :: chain_id()
|
||||
}).
|
||||
|
||||
-define(AUTH_SHARD, emqx_authn_shard).
|
||||
|
||||
-define(CLUSTER_CALL(Module, Func, Args), ?CLUSTER_CALL(Module, Func, Args, ok)).
|
||||
|
|
|
@ -22,16 +22,12 @@
|
|||
, disable/0
|
||||
]).
|
||||
|
||||
-export([authenticate/1]).
|
||||
-export([authenticate/2]).
|
||||
|
||||
-export([ create_chain/1
|
||||
, delete_chain/1
|
||||
, lookup_chain/1
|
||||
, list_chains/0
|
||||
, bind/2
|
||||
, unbind/2
|
||||
, list_bindings/1
|
||||
, list_bound_chains/1
|
||||
, create_authenticator/2
|
||||
, delete_authenticator/2
|
||||
, update_authenticator/3
|
||||
|
@ -55,10 +51,8 @@
|
|||
-boot_mnesia({mnesia, [boot]}).
|
||||
|
||||
-define(CHAIN_TAB, emqx_authn_chain).
|
||||
-define(BINDING_TAB, emqx_authn_binding).
|
||||
|
||||
-rlog_shard({?AUTH_SHARD, ?CHAIN_TAB}).
|
||||
-rlog_shard({?AUTH_SHARD, ?BINDING_TAB}).
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
%% Mnesia bootstrap
|
||||
|
@ -75,13 +69,6 @@ mnesia(boot) ->
|
|||
{record_name, chain},
|
||||
{local_content, true},
|
||||
{attributes, record_info(fields, chain)},
|
||||
{storage_properties, StoreProps}]),
|
||||
%% Binding table
|
||||
ok = ekka_mnesia:create_table(?BINDING_TAB, [
|
||||
{ram_copies, [node()]},
|
||||
{record_name, binding},
|
||||
{local_content, true},
|
||||
{attributes, record_info(fields, binding)},
|
||||
{storage_properties, StoreProps}]).
|
||||
|
||||
enable() ->
|
||||
|
@ -94,39 +81,35 @@ disable() ->
|
|||
emqx:unhook('client.authenticate', {?MODULE, authenticate, []}),
|
||||
ok.
|
||||
|
||||
authenticate(#{listener_id := ListenerID} = ClientInfo) ->
|
||||
case lookup_chain_by_listener(ListenerID, simple) of
|
||||
{error, _} ->
|
||||
{error, no_authenticators};
|
||||
{ok, ChainID} ->
|
||||
case mnesia:dirty_read(?CHAIN_TAB, ChainID) of
|
||||
[#chain{authenticators = []}] ->
|
||||
{error, no_authenticators};
|
||||
[#chain{authenticators = Authenticators}] ->
|
||||
do_authenticate(Authenticators, ClientInfo);
|
||||
[] ->
|
||||
{error, no_authenticators}
|
||||
end
|
||||
authenticate(Credential, _AuthResult) ->
|
||||
case mnesia:dirty_read(?CHAIN_TAB, ?CHAIN) of
|
||||
[#chain{authenticators = Authenticators}] ->
|
||||
do_authenticate(Authenticators, Credential);
|
||||
[] ->
|
||||
{stop, {error, not_authorized}}
|
||||
end.
|
||||
|
||||
do_authenticate([], _) ->
|
||||
{error, user_not_found};
|
||||
do_authenticate([{_, #authenticator{provider = Provider, state = State}} | More], ClientInfo) ->
|
||||
case Provider:authenticate(ClientInfo, State) of
|
||||
ignore -> do_authenticate(More, ClientInfo);
|
||||
ok -> ok;
|
||||
{ok, NewClientInfo} -> {ok, NewClientInfo};
|
||||
{stop, Reason} -> {error, Reason}
|
||||
{stop, {error, not_authorized}};
|
||||
do_authenticate([{_, #authenticator{provider = Provider, state = State}} | More], Credential) ->
|
||||
case Provider:authenticate(Credential, State) of
|
||||
ignore ->
|
||||
do_authenticate(More, Credential);
|
||||
Result ->
|
||||
%% ok
|
||||
%% {ok, AuthData}
|
||||
%% {continue, AuthCache}
|
||||
%% {continue, AuthData, AuthCache}
|
||||
%% {error, Reason}
|
||||
{stop, Result}
|
||||
end.
|
||||
|
||||
create_chain(#{id := ID,
|
||||
type := Type}) ->
|
||||
create_chain(#{id := ID}) ->
|
||||
trans(
|
||||
fun() ->
|
||||
case mnesia:read(?CHAIN_TAB, ID, write) of
|
||||
[] ->
|
||||
Chain = #chain{id = ID,
|
||||
type = Type,
|
||||
authenticators = [],
|
||||
created_at = erlang:system_time(millisecond)},
|
||||
mnesia:write(?CHAIN_TAB, Chain, write),
|
||||
|
@ -160,85 +143,20 @@ list_chains() ->
|
|||
Chains = ets:tab2list(?CHAIN_TAB),
|
||||
{ok, [serialize_chain(Chain) || Chain <- Chains]}.
|
||||
|
||||
bind(ChainID, Listeners) ->
|
||||
%% TODO: ensure listener id is valid
|
||||
trans(
|
||||
fun() ->
|
||||
case mnesia:read(?CHAIN_TAB, ChainID, write) of
|
||||
[] ->
|
||||
{error, {not_found, {chain, ChainID}}};
|
||||
[#chain{type = AuthNType}] ->
|
||||
Result = lists:foldl(
|
||||
fun(ListenerID, Acc) ->
|
||||
case mnesia:read(?BINDING_TAB, {ListenerID, AuthNType}, write) of
|
||||
[] ->
|
||||
Binding = #binding{bound = {ListenerID, AuthNType}, chain_id = ChainID},
|
||||
mnesia:write(?BINDING_TAB, Binding, write),
|
||||
Acc;
|
||||
_ ->
|
||||
[ListenerID | Acc]
|
||||
end
|
||||
end, [], Listeners),
|
||||
case Result of
|
||||
[] -> ok;
|
||||
Listeners0 -> {error, {already_bound, Listeners0}}
|
||||
end
|
||||
end
|
||||
end).
|
||||
|
||||
unbind(ChainID, Listeners) ->
|
||||
trans(
|
||||
fun() ->
|
||||
Result = lists:foldl(
|
||||
fun(ListenerID, Acc) ->
|
||||
MatchSpec = [{{binding, {ListenerID, '_'}, ChainID}, [], ['$_']}],
|
||||
case mnesia:select(?BINDING_TAB, MatchSpec, write) of
|
||||
[] ->
|
||||
[ListenerID | Acc];
|
||||
[#binding{bound = Bound}] ->
|
||||
mnesia:delete(?BINDING_TAB, Bound, write),
|
||||
Acc
|
||||
end
|
||||
end, [], Listeners),
|
||||
case Result of
|
||||
[] -> ok;
|
||||
Listeners0 ->
|
||||
{error, {not_found, Listeners0}}
|
||||
end
|
||||
end).
|
||||
|
||||
list_bindings(ChainID) ->
|
||||
trans(
|
||||
fun() ->
|
||||
MatchSpec = [{{binding, {'$1', '_'}, ChainID}, [], ['$1']}],
|
||||
Listeners = mnesia:select(?BINDING_TAB, MatchSpec),
|
||||
{ok, #{chain_id => ChainID, listeners => Listeners}}
|
||||
end).
|
||||
|
||||
list_bound_chains(ListenerID) ->
|
||||
trans(
|
||||
fun() ->
|
||||
MatchSpec = [{{binding, {ListenerID, '_'}, '_'}, [], ['$_']}],
|
||||
Bindings = mnesia:select(?BINDING_TAB, MatchSpec),
|
||||
Chains = [{AuthNType, ChainID} || #binding{bound = {_, AuthNType},
|
||||
chain_id = ChainID} <- Bindings],
|
||||
{ok, maps:from_list(Chains)}
|
||||
end).
|
||||
|
||||
create_authenticator(ChainID, #{name := Name,
|
||||
type := Type,
|
||||
mechanism := Mechanism,
|
||||
config := Config}) ->
|
||||
UpdateFun =
|
||||
fun(Chain = #chain{type = AuthNType, authenticators = Authenticators}) ->
|
||||
fun(Chain = #chain{authenticators = Authenticators}) ->
|
||||
case lists:keymember(Name, 1, Authenticators) of
|
||||
true ->
|
||||
{error, {already_exists, {authenticator, Name}}};
|
||||
false ->
|
||||
Provider = authenticator_provider(AuthNType, Type),
|
||||
Provider = authenticator_provider(Mechanism, Config),
|
||||
case Provider:create(ChainID, Name, Config) of
|
||||
{ok, State} ->
|
||||
Authenticator = #authenticator{name = Name,
|
||||
type = Type,
|
||||
mechanism = Mechanism,
|
||||
provider = Provider,
|
||||
config = Config,
|
||||
state = State},
|
||||
|
@ -367,12 +285,18 @@ list_users(ChainID, AuthenticatorName) ->
|
|||
%% Internal functions
|
||||
%%------------------------------------------------------------------------------
|
||||
|
||||
authenticator_provider(simple, 'built-in-database') -> emqx_authn_mnesia;
|
||||
authenticator_provider(simple, jwt) -> emqx_authn_jwt;
|
||||
authenticator_provider(simple, mysql) -> emqx_authn_mysql;
|
||||
authenticator_provider(simple, postgresql) -> emqx_authn_pgsql.
|
||||
|
||||
% authenticator_provider(enhanced, 'enhanced-built-in-database') -> emqx_enhanced_authn_mnesia.
|
||||
authenticator_provider('password-based', #{server_type := 'built-in-database'}) ->
|
||||
emqx_authn_mnesia;
|
||||
authenticator_provider('password-based', #{server_type := 'mysql'}) ->
|
||||
emqx_authn_mysql;
|
||||
authenticator_provider('password-based', #{server_type := 'pgsql'}) ->
|
||||
emqx_authn_pgsql;
|
||||
authenticator_provider('password-based', #{server_type := 'http-server'}) ->
|
||||
emqx_authn_http;
|
||||
authenticator_provider(jwt, _) ->
|
||||
emqx_authn_jwt;
|
||||
authenticator_provider(scram, #{server_type := 'built-in-database'}) ->
|
||||
emqx_enhanced_authn_scram_mnesia.
|
||||
|
||||
do_delete_authenticator(#authenticator{provider = Provider, state = State}) ->
|
||||
Provider:destroy(State).
|
||||
|
@ -429,13 +353,13 @@ update_chain(ChainID, UpdateFun) ->
|
|||
end
|
||||
end).
|
||||
|
||||
lookup_chain_by_listener(ListenerID, AuthNType) ->
|
||||
case mnesia:dirty_read(?BINDING_TAB, {ListenerID, AuthNType}) of
|
||||
[] ->
|
||||
{error, not_found};
|
||||
[#binding{chain_id = ChainID}] ->
|
||||
{ok, ChainID}
|
||||
end.
|
||||
% lookup_chain_by_listener(ListenerID, AuthNType) ->
|
||||
% case mnesia:dirty_read(?BINDING_TAB, {ListenerID, AuthNType}) of
|
||||
% [] ->
|
||||
% {error, not_found};
|
||||
% [#binding{chain_id = ChainID}] ->
|
||||
% {ok, ChainID}
|
||||
% end.
|
||||
|
||||
|
||||
call_authenticator(ChainID, AuthenticatorName, Func, Args) ->
|
||||
|
@ -457,11 +381,9 @@ call_authenticator(ChainID, AuthenticatorName, Func, Args) ->
|
|||
end.
|
||||
|
||||
serialize_chain(#chain{id = ID,
|
||||
type = Type,
|
||||
authenticators = Authenticators,
|
||||
created_at = CreatedAt}) ->
|
||||
#{id => ID,
|
||||
type => Type,
|
||||
authenticators => serialize_authenticators(Authenticators),
|
||||
created_at => CreatedAt}.
|
||||
|
||||
|
@ -474,10 +396,10 @@ serialize_authenticators(Authenticators) ->
|
|||
[serialize_authenticator(Authenticator) || {_, Authenticator} <- Authenticators].
|
||||
|
||||
serialize_authenticator(#authenticator{name = Name,
|
||||
type = Type,
|
||||
mechanism = Mechanism,
|
||||
config = Config}) ->
|
||||
#{name => Name,
|
||||
type => Type,
|
||||
mechanism => Mechanism,
|
||||
config => Config}.
|
||||
|
||||
trans(Fun) ->
|
||||
|
|
|
@ -18,15 +18,7 @@
|
|||
|
||||
-include("emqx_authn.hrl").
|
||||
|
||||
-export([ create_chain/2
|
||||
, delete_chain/2
|
||||
, lookup_chain/2
|
||||
, list_chains/2
|
||||
, bind/2
|
||||
, unbind/2
|
||||
, list_bindings/2
|
||||
, list_bound_chains/2
|
||||
, create_authenticator/2
|
||||
-export([ create_authenticator/2
|
||||
, delete_authenticator/2
|
||||
, update_authenticator/2
|
||||
, lookup_authenticator/2
|
||||
|
@ -40,135 +32,79 @@
|
|||
, list_users/2
|
||||
]).
|
||||
|
||||
-rest_api(#{name => create_chain,
|
||||
method => 'POST',
|
||||
path => "/authentication/chains",
|
||||
func => create_chain,
|
||||
descr => "Create a chain"
|
||||
}).
|
||||
|
||||
-rest_api(#{name => delete_chain,
|
||||
method => 'DELETE',
|
||||
path => "/authentication/chains/:bin:id",
|
||||
func => delete_chain,
|
||||
descr => "Delete chain"
|
||||
}).
|
||||
|
||||
-rest_api(#{name => lookup_chain,
|
||||
method => 'GET',
|
||||
path => "/authentication/chains/:bin:id",
|
||||
func => lookup_chain,
|
||||
descr => "Lookup chain"
|
||||
}).
|
||||
|
||||
-rest_api(#{name => list_chains,
|
||||
method => 'GET',
|
||||
path => "/authentication/chains",
|
||||
func => list_chains,
|
||||
descr => "List all chains"
|
||||
}).
|
||||
|
||||
-rest_api(#{name => bind,
|
||||
method => 'POST',
|
||||
path => "/authentication/chains/:bin:id/bindings/bulk",
|
||||
func => bind,
|
||||
descr => "Bind"
|
||||
}).
|
||||
|
||||
-rest_api(#{name => unbind,
|
||||
method => 'DELETE',
|
||||
path => "/authentication/chains/:bin:id/bindings/bulk",
|
||||
func => unbind,
|
||||
descr => "Unbind"
|
||||
}).
|
||||
|
||||
-rest_api(#{name => list_bindings,
|
||||
method => 'GET',
|
||||
path => "/authentication/chains/:bin:id/bindings",
|
||||
func => list_bindings,
|
||||
descr => "List bindings"
|
||||
}).
|
||||
|
||||
-rest_api(#{name => list_bound_chains,
|
||||
method => 'GET',
|
||||
path => "/authentication/listeners/:bin:listener_id/bound_chains",
|
||||
func => list_bound_chains,
|
||||
descr => "List bound chains"
|
||||
}).
|
||||
|
||||
-rest_api(#{name => create_authenticator,
|
||||
method => 'POST',
|
||||
path => "/authentication/chains/:bin:id/authenticators",
|
||||
path => "/authentication/authenticators",
|
||||
func => create_authenticator,
|
||||
descr => "Create authenticator to chain"
|
||||
descr => "Create authenticator"
|
||||
}).
|
||||
|
||||
-rest_api(#{name => delete_authenticator,
|
||||
method => 'DELETE',
|
||||
path => "/authentication/chains/:bin:id/authenticators/:bin:authenticator_name",
|
||||
path => "/authentication/authenticators/:bin:name",
|
||||
func => delete_authenticator,
|
||||
descr => "Delete authenticator from chain"
|
||||
descr => "Delete authenticator"
|
||||
}).
|
||||
|
||||
-rest_api(#{name => update_authenticator,
|
||||
method => 'PUT',
|
||||
path => "/authentication/chains/:bin:id/authenticators/:bin:authenticator_name",
|
||||
path => "/authentication/authenticators/:bin:name",
|
||||
func => update_authenticator,
|
||||
descr => "Update authenticator in chain"
|
||||
descr => "Update authenticator"
|
||||
}).
|
||||
|
||||
-rest_api(#{name => lookup_authenticator,
|
||||
method => 'GET',
|
||||
path => "/authentication/chains/:bin:id/authenticators/:bin:authenticator_name",
|
||||
path => "/authentication/authenticators/:bin:name",
|
||||
func => lookup_authenticator,
|
||||
descr => "Lookup authenticator in chain"
|
||||
descr => "Lookup authenticator"
|
||||
}).
|
||||
|
||||
-rest_api(#{name => list_authenticators,
|
||||
method => 'GET',
|
||||
path => "/authentication/chains/:bin:id/authenticators",
|
||||
path => "/authentication/authenticators",
|
||||
func => list_authenticators,
|
||||
descr => "List authenticators in chain"
|
||||
descr => "List authenticators"
|
||||
}).
|
||||
|
||||
-rest_api(#{name => move_authenticator,
|
||||
method => 'POST',
|
||||
path => "/authentication/chains/:bin:id/authenticators/:bin:authenticator_name/position",
|
||||
path => "/authentication/authenticators/:bin:name/position",
|
||||
func => move_authenticator,
|
||||
descr => "Change the order of authenticators"
|
||||
}).
|
||||
|
||||
-rest_api(#{name => import_users,
|
||||
method => 'POST',
|
||||
path => "/authentication/chains/:bin:id/authenticators/:bin:authenticator_name/import-users",
|
||||
path => "/authentication/authenticators/:bin:name/import-users",
|
||||
func => import_users,
|
||||
descr => "Import users"
|
||||
}).
|
||||
|
||||
-rest_api(#{name => add_user,
|
||||
method => 'POST',
|
||||
path => "/authentication/chains/:bin:id/authenticators/:bin:authenticator_name/users",
|
||||
path => "/authentication/authenticators/:bin:name/users",
|
||||
func => add_user,
|
||||
descr => "Add user"
|
||||
}).
|
||||
|
||||
-rest_api(#{name => delete_user,
|
||||
method => 'DELETE',
|
||||
path => "/authentication/chains/:bin:id/authenticators/:bin:authenticator_name/users/:bin:user_id",
|
||||
path => "/authentication/authenticators/:bin:name/users/:bin:user_id",
|
||||
func => delete_user,
|
||||
descr => "Delete user"
|
||||
}).
|
||||
|
||||
-rest_api(#{name => update_user,
|
||||
method => 'PUT',
|
||||
path => "/authentication/chains/:bin:id/authenticators/:bin:authenticator_name/users/:bin:user_id",
|
||||
path => "/authentication/authenticators/:bin:name/users/:bin:user_id",
|
||||
func => update_user,
|
||||
descr => "Update user"
|
||||
}).
|
||||
|
||||
-rest_api(#{name => lookup_user,
|
||||
method => 'GET',
|
||||
path => "/authentication/chains/:bin:id/authenticators/:bin:authenticator_name/users/:bin:user_id",
|
||||
path => "/authentication/authenticators/:bin:name/users/:bin:user_id",
|
||||
func => lookup_user,
|
||||
descr => "Lookup user"
|
||||
}).
|
||||
|
@ -176,129 +112,24 @@
|
|||
%% TODO: Support pagination
|
||||
-rest_api(#{name => list_users,
|
||||
method => 'GET',
|
||||
path => "/authentication/chains/:bin:id/authenticators/:bin:authenticator_name/users",
|
||||
path => "/authentication/authenticators/:bin:name/users",
|
||||
func => list_users,
|
||||
descr => "List all users"
|
||||
}).
|
||||
|
||||
create_chain(Binding, Params) ->
|
||||
do_create_chain(uri_decode(Binding), maps:from_list(Params)).
|
||||
|
||||
do_create_chain(_Binding, Chain0) ->
|
||||
Config = #{<<"authn">> => #{<<"chains">> => [Chain0#{<<"authenticators">> => []}],
|
||||
<<"bindings">> => []}},
|
||||
#{authn := #{chains := [Chain1]}}
|
||||
= hocon_schema:check_plain(emqx_authn_schema, Config,
|
||||
#{atom_key => true, nullable => true}),
|
||||
case emqx_authn:create_chain(Chain1) of
|
||||
{ok, Chain2} ->
|
||||
return({ok, Chain2});
|
||||
{error, Reason} ->
|
||||
return(serialize_error(Reason))
|
||||
end.
|
||||
|
||||
delete_chain(Binding, Params) ->
|
||||
do_delete_chain(uri_decode(Binding), maps:from_list(Params)).
|
||||
|
||||
do_delete_chain(#{id := ChainID}, _Params) ->
|
||||
case emqx_authn:delete_chain(ChainID) of
|
||||
ok ->
|
||||
return(ok);
|
||||
{error, Reason} ->
|
||||
return(serialize_error(Reason))
|
||||
end.
|
||||
|
||||
lookup_chain(Binding, Params) ->
|
||||
do_lookup_chain(uri_decode(Binding), maps:from_list(Params)).
|
||||
|
||||
do_lookup_chain(#{id := ChainID}, _Params) ->
|
||||
case emqx_authn:lookup_chain(ChainID) of
|
||||
{ok, Chain} ->
|
||||
return({ok, Chain});
|
||||
{error, Reason} ->
|
||||
return(serialize_error(Reason))
|
||||
end.
|
||||
|
||||
list_chains(Binding, Params) ->
|
||||
do_list_chains(uri_decode(Binding), maps:from_list(Params)).
|
||||
|
||||
do_list_chains(_Binding, _Params) ->
|
||||
{ok, Chains} = emqx_authn:list_chains(),
|
||||
return({ok, Chains}).
|
||||
|
||||
bind(Binding, Params) ->
|
||||
do_bind(uri_decode(Binding), lists_to_map(Params)).
|
||||
|
||||
do_bind(#{id := ChainID}, #{<<"listeners">> := Listeners}) ->
|
||||
% Config = #{<<"authn">> => #{<<"chains">> => [],
|
||||
% <<"bindings">> => [#{<<"chain">> := ChainID,
|
||||
% <<"listeners">> := Listeners}]}},
|
||||
% #{authn := #{bindings := [#{listeners := Listeners}]}}
|
||||
% = hocon_schema:check_plain(emqx_authn_schema, Config,
|
||||
% #{atom_key => true, nullable => true}),
|
||||
case emqx_authn:bind(ChainID, Listeners) of
|
||||
ok ->
|
||||
return(ok);
|
||||
{error, {alread_bound, Listeners}} ->
|
||||
{ok, #{code => <<"ALREADY_EXISTS">>,
|
||||
message => <<"ALREADY_BOUND">>,
|
||||
detail => Listeners}};
|
||||
{error, Reason} ->
|
||||
return(serialize_error(Reason))
|
||||
end;
|
||||
do_bind(_, _) ->
|
||||
return(serialize_error({missing_parameter, <<"listeners">>})).
|
||||
|
||||
unbind(Binding, Params) ->
|
||||
do_unbind(uri_decode(Binding), lists_to_map(Params)).
|
||||
|
||||
do_unbind(#{id := ChainID}, #{<<"listeners">> := Listeners0}) ->
|
||||
case emqx_authn:unbind(ChainID, Listeners0) of
|
||||
ok ->
|
||||
return(ok);
|
||||
{error, {not_found, Listeners1}} ->
|
||||
{ok, #{code => <<"NOT_FOUND">>,
|
||||
detail => Listeners1}};
|
||||
{error, Reason} ->
|
||||
return(serialize_error(Reason))
|
||||
end;
|
||||
do_unbind(_, _) ->
|
||||
return(serialize_error({missing_parameter, <<"listeners">>})).
|
||||
|
||||
list_bindings(Binding, Params) ->
|
||||
do_list_bindings(uri_decode(Binding), lists_to_map(Params)).
|
||||
|
||||
do_list_bindings(#{id := ChainID}, _) ->
|
||||
{ok, Binding} = emqx_authn:list_bindings(ChainID),
|
||||
return({ok, Binding}).
|
||||
|
||||
list_bound_chains(Binding, Params) ->
|
||||
do_list_bound_chains(uri_decode(Binding), lists_to_map(Params)).
|
||||
|
||||
do_list_bound_chains(#{listener_id := ListenerID}, _) ->
|
||||
{ok, Chains} = emqx_authn:list_bound_chains(ListenerID),
|
||||
return({ok, Chains}).
|
||||
|
||||
create_authenticator(Binding, Params) ->
|
||||
do_create_authenticator(uri_decode(Binding), lists_to_map(Params)).
|
||||
|
||||
do_create_authenticator(#{id := ChainID}, Authenticator0) ->
|
||||
case emqx_authn:lookup_chain(ChainID) of
|
||||
{ok, #{type := Type}} ->
|
||||
Chain = #{<<"id">> => ChainID,
|
||||
<<"type">> => Type,
|
||||
<<"authenticators">> => [Authenticator0]},
|
||||
Config = #{<<"authn">> => #{<<"chains">> => [Chain],
|
||||
<<"bindings">> => []}},
|
||||
#{authn := #{chains := [#{authenticators := [Authenticator1]}]}}
|
||||
= hocon_schema:check_plain(emqx_authn_schema, Config,
|
||||
#{atom_key => true, nullable => true}),
|
||||
case emqx_authn:create_authenticator(ChainID, Authenticator1) of
|
||||
{ok, Authenticator2} ->
|
||||
return({ok, Authenticator2});
|
||||
{error, Reason} ->
|
||||
return(serialize_error(Reason))
|
||||
end;
|
||||
do_create_authenticator(_Binding, Authenticator0) ->
|
||||
Config = #{<<"emqx_authn">> => #{
|
||||
<<"authenticators">> => [Authenticator0]
|
||||
}},
|
||||
#{emqx_authn := #{authenticators := [Authenticator1]}}
|
||||
= hocon_schema:check_plain(emqx_authn_schema, Config,
|
||||
#{atom_key => true, nullable => true}),
|
||||
case emqx_authn:create_authenticator(?CHAIN, Authenticator1) of
|
||||
{ok, Authenticator2} ->
|
||||
return({ok, Authenticator2});
|
||||
{error, Reason} ->
|
||||
return(serialize_error(Reason))
|
||||
end.
|
||||
|
@ -306,9 +137,8 @@ do_create_authenticator(#{id := ChainID}, Authenticator0) ->
|
|||
delete_authenticator(Binding, Params) ->
|
||||
do_delete_authenticator(uri_decode(Binding), maps:from_list(Params)).
|
||||
|
||||
do_delete_authenticator(#{id := ChainID,
|
||||
authenticator_name := AuthenticatorName}, _Params) ->
|
||||
case emqx_authn:delete_authenticator(ChainID, AuthenticatorName) of
|
||||
do_delete_authenticator(#{name := Name}, _Params) ->
|
||||
case emqx_authn:delete_authenticator(?CHAIN, Name) of
|
||||
ok ->
|
||||
return(ok);
|
||||
{error, Reason} ->
|
||||
|
@ -320,36 +150,26 @@ update_authenticator(Binding, Params) ->
|
|||
do_update_authenticator(uri_decode(Binding), lists_to_map(Params)).
|
||||
|
||||
%% TOOD: PUT method supports creation and update
|
||||
do_update_authenticator(#{id := ChainID,
|
||||
authenticator_name := AuthenticatorName}, AuthenticatorConfig0) ->
|
||||
case emqx_authn:lookup_chain(ChainID) of
|
||||
{ok, #{type := ChainType}} ->
|
||||
case emqx_authn:lookup_authenticator(ChainID, AuthenticatorName) of
|
||||
{ok, #{type := Type}} ->
|
||||
Authenticator = #{<<"name">> => AuthenticatorName,
|
||||
<<"type">> => Type,
|
||||
<<"config">> => AuthenticatorConfig0},
|
||||
Chain = #{<<"id">> => ChainID,
|
||||
<<"type">> => ChainType,
|
||||
<<"authenticators">> => [Authenticator]},
|
||||
Config = #{<<"authn">> => #{<<"chains">> => [Chain],
|
||||
<<"bindings">> => []}},
|
||||
#{
|
||||
authn := #{
|
||||
chains := [#{
|
||||
authenticators := [#{
|
||||
config := AuthenticatorConfig1
|
||||
}]
|
||||
}]
|
||||
}
|
||||
} = hocon_schema:check_plain(emqx_authn_schema, Config,
|
||||
#{atom_key => true, nullable => true}),
|
||||
case emqx_authn:update_authenticator(ChainID, AuthenticatorName, AuthenticatorConfig1) of
|
||||
{ok, NAuthenticator} ->
|
||||
return({ok, NAuthenticator});
|
||||
{error, Reason} ->
|
||||
return(serialize_error(Reason))
|
||||
end;
|
||||
do_update_authenticator(#{name := Name}, NewConfig0) ->
|
||||
case emqx_authn:lookup_authenticator(?CHAIN, Name) of
|
||||
{ok, #{mechanism := Mechanism}} ->
|
||||
Authenticator = #{<<"name">> => Name,
|
||||
<<"mechanism">> => Mechanism,
|
||||
<<"config">> => NewConfig0},
|
||||
Config = #{<<"emqx_authn">> => #{
|
||||
<<"authenticators">> => [Authenticator]
|
||||
}},
|
||||
#{
|
||||
emqx_authn := #{
|
||||
authenticators := [#{
|
||||
config := NewConfig1
|
||||
}]
|
||||
}
|
||||
} = hocon_schema:check_plain(emqx_authn_schema, Config,
|
||||
#{atom_key => true, nullable => true}),
|
||||
case emqx_authn:update_authenticator(?CHAIN, Name, NewConfig1) of
|
||||
{ok, NAuthenticator} ->
|
||||
return({ok, NAuthenticator});
|
||||
{error, Reason} ->
|
||||
return(serialize_error(Reason))
|
||||
end;
|
||||
|
@ -360,9 +180,8 @@ do_update_authenticator(#{id := ChainID,
|
|||
lookup_authenticator(Binding, Params) ->
|
||||
do_lookup_authenticator(uri_decode(Binding), maps:from_list(Params)).
|
||||
|
||||
do_lookup_authenticator(#{id := ChainID,
|
||||
authenticator_name := AuthenticatorName}, _Params) ->
|
||||
case emqx_authn:lookup_authenticator(ChainID, AuthenticatorName) of
|
||||
do_lookup_authenticator(#{name := Name}, _Params) ->
|
||||
case emqx_authn:lookup_authenticator(?CHAIN, Name) of
|
||||
{ok, Authenticator} ->
|
||||
return({ok, Authenticator});
|
||||
{error, Reason} ->
|
||||
|
@ -372,8 +191,8 @@ do_lookup_authenticator(#{id := ChainID,
|
|||
list_authenticators(Binding, Params) ->
|
||||
do_list_authenticators(uri_decode(Binding), maps:from_list(Params)).
|
||||
|
||||
do_list_authenticators(#{id := ChainID}, _Params) ->
|
||||
case emqx_authn:list_authenticators(ChainID) of
|
||||
do_list_authenticators(_Binding, _Params) ->
|
||||
case emqx_authn:list_authenticators(?CHAIN) of
|
||||
{ok, Authenticators} ->
|
||||
return({ok, Authenticators});
|
||||
{error, Reason} ->
|
||||
|
@ -383,25 +202,22 @@ do_list_authenticators(#{id := ChainID}, _Params) ->
|
|||
move_authenticator(Binding, Params) ->
|
||||
do_move_authenticator(uri_decode(Binding), maps:from_list(Params)).
|
||||
|
||||
do_move_authenticator(#{id := ChainID,
|
||||
authenticator_name := AuthenticatorName}, #{<<"position">> := <<"the front">>}) ->
|
||||
case emqx_authn:move_authenticator_to_the_front(ChainID, AuthenticatorName) of
|
||||
do_move_authenticator(#{name := Name}, #{<<"position">> := <<"the front">>}) ->
|
||||
case emqx_authn:move_authenticator_to_the_front(?CHAIN, Name) of
|
||||
ok ->
|
||||
return(ok);
|
||||
{error, Reason} ->
|
||||
return(serialize_error(Reason))
|
||||
end;
|
||||
do_move_authenticator(#{id := ChainID,
|
||||
authenticator_name := AuthenticatorName}, #{<<"position">> := <<"the end">>}) ->
|
||||
case emqx_authn:move_authenticator_to_the_end(ChainID, AuthenticatorName) of
|
||||
do_move_authenticator(#{name := Name}, #{<<"position">> := <<"the end">>}) ->
|
||||
case emqx_authn:move_authenticator_to_the_end(?CHAIN, Name) of
|
||||
ok ->
|
||||
return(ok);
|
||||
{error, Reason} ->
|
||||
return(serialize_error(Reason))
|
||||
end;
|
||||
do_move_authenticator(#{id := ChainID,
|
||||
authenticator_name := AuthenticatorName}, #{<<"position">> := N}) when is_number(N) ->
|
||||
case emqx_authn:move_authenticator_to_the_nth(ChainID, AuthenticatorName, N) of
|
||||
do_move_authenticator(#{name := Name}, #{<<"position">> := N}) when is_number(N) ->
|
||||
case emqx_authn:move_authenticator_to_the_nth(?CHAIN, Name, N) of
|
||||
ok ->
|
||||
return(ok);
|
||||
{error, Reason} ->
|
||||
|
@ -413,9 +229,9 @@ do_move_authenticator(_Binding, _Params) ->
|
|||
import_users(Binding, Params) ->
|
||||
do_import_users(uri_decode(Binding), maps:from_list(Params)).
|
||||
|
||||
do_import_users(#{id := ChainID, authenticator_name := AuthenticatorName},
|
||||
do_import_users(#{name := Name},
|
||||
#{<<"filename">> := Filename}) ->
|
||||
case emqx_authn:import_users(ChainID, AuthenticatorName, Filename) of
|
||||
case emqx_authn:import_users(?CHAIN, Name, Filename) of
|
||||
ok ->
|
||||
return(ok);
|
||||
{error, Reason} ->
|
||||
|
@ -428,9 +244,8 @@ do_import_users(_Binding, Params) ->
|
|||
add_user(Binding, Params) ->
|
||||
do_add_user(uri_decode(Binding), maps:from_list(Params)).
|
||||
|
||||
do_add_user(#{id := ChainID,
|
||||
authenticator_name := AuthenticatorName}, UserInfo) ->
|
||||
case emqx_authn:add_user(ChainID, AuthenticatorName, UserInfo) of
|
||||
do_add_user(#{name := Name}, UserInfo) ->
|
||||
case emqx_authn:add_user(?CHAIN, Name, UserInfo) of
|
||||
{ok, User} ->
|
||||
return({ok, User});
|
||||
{error, Reason} ->
|
||||
|
@ -440,10 +255,9 @@ do_add_user(#{id := ChainID,
|
|||
delete_user(Binding, Params) ->
|
||||
do_delete_user(uri_decode(Binding), maps:from_list(Params)).
|
||||
|
||||
do_delete_user(#{id := ChainID,
|
||||
authenticator_name := AuthenticatorName,
|
||||
do_delete_user(#{name := Name,
|
||||
user_id := UserID}, _Params) ->
|
||||
case emqx_authn:delete_user(ChainID, AuthenticatorName, UserID) of
|
||||
case emqx_authn:delete_user(?CHAIN, Name, UserID) of
|
||||
ok ->
|
||||
return(ok);
|
||||
{error, Reason} ->
|
||||
|
@ -453,10 +267,9 @@ do_delete_user(#{id := ChainID,
|
|||
update_user(Binding, Params) ->
|
||||
do_update_user(uri_decode(Binding), maps:from_list(Params)).
|
||||
|
||||
do_update_user(#{id := ChainID,
|
||||
authenticator_name := AuthenticatorName,
|
||||
do_update_user(#{name := Name,
|
||||
user_id := UserID}, NewUserInfo) ->
|
||||
case emqx_authn:update_user(ChainID, AuthenticatorName, UserID, NewUserInfo) of
|
||||
case emqx_authn:update_user(?CHAIN, Name, UserID, NewUserInfo) of
|
||||
{ok, User} ->
|
||||
return({ok, User});
|
||||
{error, Reason} ->
|
||||
|
@ -466,10 +279,9 @@ do_update_user(#{id := ChainID,
|
|||
lookup_user(Binding, Params) ->
|
||||
do_lookup_user(uri_decode(Binding), maps:from_list(Params)).
|
||||
|
||||
do_lookup_user(#{id := ChainID,
|
||||
authenticator_name := AuthenticatorName,
|
||||
do_lookup_user(#{name := Name,
|
||||
user_id := UserID}, _Params) ->
|
||||
case emqx_authn:lookup_user(ChainID, AuthenticatorName, UserID) of
|
||||
case emqx_authn:lookup_user(?CHAIN, Name, UserID) of
|
||||
{ok, User} ->
|
||||
return({ok, User});
|
||||
{error, Reason} ->
|
||||
|
@ -479,9 +291,8 @@ do_lookup_user(#{id := ChainID,
|
|||
list_users(Binding, Params) ->
|
||||
do_list_users(uri_decode(Binding), maps:from_list(Params)).
|
||||
|
||||
do_list_users(#{id := ChainID,
|
||||
authenticator_name := AuthenticatorName}, _Params) ->
|
||||
case emqx_authn:list_users(ChainID, AuthenticatorName) of
|
||||
do_list_users(#{name := Name}, _Params) ->
|
||||
case emqx_authn:list_users(?CHAIN, Name) of
|
||||
{ok, Users} ->
|
||||
return({ok, Users});
|
||||
{error, Reason} ->
|
||||
|
@ -526,11 +337,7 @@ serialize_error(_) ->
|
|||
{error, <<"UNKNOWN_ERROR">>, <<"Unknown error">>}.
|
||||
|
||||
serialize_type(authenticator) ->
|
||||
"Authenticator";
|
||||
serialize_type(chain) ->
|
||||
"Chain";
|
||||
serialize_type(authenticator_type) ->
|
||||
"Authenticator type".
|
||||
"Authenticator".
|
||||
|
||||
get_missed_params(Actual, Expected) ->
|
||||
Keys = lists:foldl(fun(Key, Acc) ->
|
||||
|
|
|
@ -38,40 +38,19 @@ stop(_State) ->
|
|||
ok.
|
||||
|
||||
initialize() ->
|
||||
#{chains := Chains,
|
||||
bindings := Bindings} = emqx_config:get([authn], #{chains => [], bindings => []}),
|
||||
initialize_chains(Chains),
|
||||
initialize_bindings(Bindings).
|
||||
#{authenticators := Authenticators} = emqx_config:get([emqx_authn], #{authenticators => []}),
|
||||
initialize(Authenticators).
|
||||
|
||||
initialize_chains([]) ->
|
||||
initialize(Authenticators) ->
|
||||
{ok, _} = emqx_authn:create_chain(#{id => ?CHAIN}),
|
||||
initialize_authenticators(Authenticators).
|
||||
|
||||
initialize_authenticators([]) ->
|
||||
ok;
|
||||
initialize_chains([#{id := ChainID,
|
||||
type := Type,
|
||||
authenticators := Authenticators} | More]) ->
|
||||
case emqx_authn:create_chain(#{id => ChainID,
|
||||
type => Type}) of
|
||||
initialize_authenticators([#{name := Name} = Authenticator | More]) ->
|
||||
case emqx_authn:create_authenticator(?CHAIN, Authenticator) of
|
||||
{ok, _} ->
|
||||
initialize_authenticators(ChainID, Authenticators),
|
||||
initialize_chains(More);
|
||||
initialize_authenticators(More);
|
||||
{error, Reason} ->
|
||||
?LOG(error, "Failed to create chain '~s': ~p", [ChainID, Reason])
|
||||
end.
|
||||
|
||||
initialize_authenticators(_ChainID, []) ->
|
||||
ok;
|
||||
initialize_authenticators(ChainID, [#{name := Name} = Authenticator | More]) ->
|
||||
case emqx_authn:create_authenticator(ChainID, Authenticator) of
|
||||
{ok, _} ->
|
||||
initialize_authenticators(ChainID, More);
|
||||
{error, Reason} ->
|
||||
?LOG(error, "Failed to create authenticator '~s' in chain '~s': ~p", [Name, ChainID, Reason])
|
||||
end.
|
||||
|
||||
initialize_bindings([]) ->
|
||||
ok;
|
||||
initialize_bindings([#{chain_id := ChainID, listeners := Listeners} | More]) ->
|
||||
case emqx_authn:bind(Listeners, ChainID) of
|
||||
ok -> initialize_bindings(More);
|
||||
{error, Reason} ->
|
||||
?LOG(error, "Failed to bind: ~p", [Reason])
|
||||
end.
|
||||
?LOG(error, "Failed to create authenticator '~s': ~p", [Name, Reason])
|
||||
end.
|
|
@ -21,94 +21,55 @@
|
|||
|
||||
-behaviour(hocon_schema).
|
||||
|
||||
-export([structs/0, fields/1]).
|
||||
-export([ structs/0
|
||||
, fields/1
|
||||
]).
|
||||
|
||||
-reflect_type([ chain_id/0
|
||||
, authenticator_name/0
|
||||
-reflect_type([ authenticator_name/0
|
||||
]).
|
||||
|
||||
structs() -> ["emqx_authn"].
|
||||
|
||||
fields("emqx_authn") ->
|
||||
[ {chains, fun chains/1}
|
||||
, {bindings, fun bindings/1}];
|
||||
[ {authenticators, fun authenticators/1} ];
|
||||
|
||||
fields('simple-chain') ->
|
||||
[ {id, fun chain_id/1}
|
||||
, {type, {enum, [simple]}}
|
||||
, {authenticators, fun simple_authenticators/1}
|
||||
fields('password-based') ->
|
||||
[ {name, fun authenticator_name/1}
|
||||
, {mechanism, {enum, ['password-based']}}
|
||||
, {config, hoconsc:t(hoconsc:union(
|
||||
[ hoconsc:ref(emqx_authn_mnesia, config)
|
||||
, hoconsc:ref(emqx_authn_mysql, config)
|
||||
, hoconsc:ref(emqx_authn_pgsql, config)
|
||||
, hoconsc:ref(emqx_authn_http, get)
|
||||
, hoconsc:ref(emqx_authn_http, post)
|
||||
]))}
|
||||
];
|
||||
|
||||
% fields('enhanced-chain') ->
|
||||
% [ {id, fun chain_id/1}
|
||||
% , {type, {enum, [enhanced]}}
|
||||
% , {authenticators, fun enhanced_authenticators/1}
|
||||
% ];
|
||||
|
||||
fields(binding) ->
|
||||
[ {chain_id, fun chain_id/1}
|
||||
, {listeners, fun listeners/1}
|
||||
];
|
||||
|
||||
fields('built-in-database') ->
|
||||
[ {name, fun authenticator_name/1}
|
||||
, {type, {enum, ['built-in-database']}}
|
||||
, {config, hoconsc:t(hoconsc:ref(emqx_authn_mnesia, config))}
|
||||
];
|
||||
|
||||
% fields('enhanced-built-in-database') ->
|
||||
% [ {name, fun authenticator_name/1}
|
||||
% , {type, {enum, ['built-in-database']}}
|
||||
% , {config, hoconsc:t(hoconsc:ref(emqx_enhanced_authn_mnesia, config))}
|
||||
% ];
|
||||
|
||||
fields(jwt) ->
|
||||
[ {name, fun authenticator_name/1}
|
||||
, {type, {enum, [jwt]}}
|
||||
, {config, hoconsc:t(hoconsc:ref(emqx_authn_jwt, config))}
|
||||
[ {name, fun authenticator_name/1}
|
||||
, {mechanism, {enum, [jwt]}}
|
||||
, {config, hoconsc:t(hoconsc:union(
|
||||
[ hoconsc:ref(emqx_authn_jwt, 'hmac-based')
|
||||
, hoconsc:ref(emqx_authn_jwt, 'public-key')
|
||||
, hoconsc:ref(emqx_authn_jwt, 'jwks')
|
||||
]))}
|
||||
];
|
||||
|
||||
fields(mysql) ->
|
||||
[ {name, fun authenticator_name/1}
|
||||
, {type, {enum, [mysql]}}
|
||||
, {config, hoconsc:t(hoconsc:ref(emqx_authn_mysql, config))}
|
||||
];
|
||||
|
||||
fields(pgsql) ->
|
||||
[ {name, fun authenticator_name/1}
|
||||
, {type, {enum, [postgresql]}}
|
||||
, {config, hoconsc:t(hoconsc:ref(emqx_authn_pgsql, config))}
|
||||
fields(scram) ->
|
||||
[ {name, fun authenticator_name/1}
|
||||
, {mechanism, {enum, [scram]}}
|
||||
, {config, hoconsc:t(hoconsc:union(
|
||||
[ hoconsc:ref(emqx_enhanced_authn_scram_mnesia, config)
|
||||
]))}
|
||||
].
|
||||
|
||||
chains(type) -> hoconsc:array({union, [hoconsc:ref(?MODULE, 'simple-chain')]});
|
||||
chains(default) -> [];
|
||||
chains(_) -> undefined.
|
||||
|
||||
chain_id(type) -> chain_id();
|
||||
chain_id(nullable) -> false;
|
||||
chain_id(_) -> undefined.
|
||||
|
||||
simple_authenticators(type) ->
|
||||
hoconsc:array({union, [ hoconsc:ref(?MODULE, 'built-in-database')
|
||||
authenticators(type) ->
|
||||
hoconsc:array({union, [ hoconsc:ref(?MODULE, 'password-based')
|
||||
, hoconsc:ref(?MODULE, jwt)
|
||||
, hoconsc:ref(?MODULE, mysql)
|
||||
, hoconsc:ref(?MODULE, pgsql)]});
|
||||
simple_authenticators(default) -> [];
|
||||
simple_authenticators(_) -> undefined.
|
||||
|
||||
% enhanced_authenticators(type) ->
|
||||
% hoconsc:array({union, [hoconsc:ref('enhanced-built-in-database')]});
|
||||
% enhanced_authenticators(default) -> [];
|
||||
% enhanced_authenticators(_) -> undefined.
|
||||
, hoconsc:ref(?MODULE, scram)]});
|
||||
authenticators(default) -> [];
|
||||
authenticators(_) -> undefined.
|
||||
|
||||
authenticator_name(type) -> authenticator_name();
|
||||
authenticator_name(nullable) -> false;
|
||||
authenticator_name(_) -> undefined.
|
||||
|
||||
bindings(type) -> hoconsc:array(hoconsc:ref(?MODULE, binding));
|
||||
bindings(default) -> [];
|
||||
bindings(_) -> undefined.
|
||||
|
||||
listeners(type) -> hoconsc:array(binary());
|
||||
listeners(default) -> [];
|
||||
listeners(_) -> undefined.
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
-module(emqx_authn_utils).
|
||||
|
||||
-export([ replace_placeholder/2
|
||||
, gen_salt/0
|
||||
]).
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
|
@ -41,6 +42,10 @@ replace_placeholder([<<"${cert-common-name}">> | More], #{cn := CommonName} = Da
|
|||
replace_placeholder([_ | More], Data, Acc) ->
|
||||
replace_placeholder(More, Data, [null | Acc]).
|
||||
|
||||
gen_salt() ->
|
||||
<<X:128/big-unsigned-integer>> = crypto:strong_rand_bytes(16),
|
||||
iolist_to_binary(io_lib:format("~32.16.0b", [X])).
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
%% Internal functions
|
||||
%%------------------------------------------------------------------------------
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2021 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_enhanced_authn_mnesia).
|
|
@ -0,0 +1,240 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2021 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_enhanced_authn_scram_mnesia).
|
||||
|
||||
-include("emqx_authn.hrl").
|
||||
-include_lib("esasl/include/esasl_scram.hrl").
|
||||
-include_lib("typerefl/include/types.hrl").
|
||||
|
||||
-behaviour(hocon_schema).
|
||||
|
||||
-export([ structs/0
|
||||
, fields/1
|
||||
]).
|
||||
|
||||
-export([ create/3
|
||||
, update/4
|
||||
, authenticate/2
|
||||
, destroy/1
|
||||
]).
|
||||
|
||||
-export([ add_user/2
|
||||
, delete_user/2
|
||||
, update_user/3
|
||||
, lookup_user/2
|
||||
, list_users/1
|
||||
]).
|
||||
|
||||
-define(TAB, ?MODULE).
|
||||
|
||||
-export([mnesia/1]).
|
||||
|
||||
-boot_mnesia({mnesia, [boot]}).
|
||||
-copy_mnesia({mnesia, [copy]}).
|
||||
|
||||
-rlog_shard({?AUTH_SHARD, ?TAB}).
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
%% Mnesia bootstrap
|
||||
%%------------------------------------------------------------------------------
|
||||
|
||||
%% @doc Create or replicate tables.
|
||||
-spec(mnesia(boot | copy) -> ok).
|
||||
mnesia(boot) ->
|
||||
ok = ekka_mnesia:create_table(?TAB, [
|
||||
{disc_copies, [node()]},
|
||||
{record_name, scram_user_credentail},
|
||||
{attributes, record_info(fields, scram_user_credentail)},
|
||||
{storage_properties, [{ets, [{read_concurrency, true}]}]}]);
|
||||
|
||||
mnesia(copy) ->
|
||||
ok = ekka_mnesia:copy_table(?TAB, disc_copies).
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
%% Hocon Schema
|
||||
%%------------------------------------------------------------------------------
|
||||
|
||||
structs() -> [config].
|
||||
|
||||
fields(config) ->
|
||||
[ {server_type, fun server_type/1}
|
||||
, {algorithm, fun algorithm/1}
|
||||
, {iteration_count, fun iteration_count/1}
|
||||
].
|
||||
|
||||
server_type(type) -> hoconsc:enum(['built-in-database']);
|
||||
server_type(default) -> 'built-in-database';
|
||||
server_type(_) -> undefined.
|
||||
|
||||
algorithm(type) -> hoconsc:enum([sha256, sha256]);
|
||||
algorithm(default) -> sha256;
|
||||
algorithm(_) -> undefined.
|
||||
|
||||
iteration_count(type) -> non_neg_integer();
|
||||
iteration_count(default) -> 4096;
|
||||
iteration_count(_) -> undefined.
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
%% APIs
|
||||
%%------------------------------------------------------------------------------
|
||||
|
||||
create(ChainID, Authenticator, #{algorithm := Algorithm,
|
||||
iteration_count := IterationCount}) ->
|
||||
State = #{user_group => {ChainID, Authenticator},
|
||||
algorithm => Algorithm,
|
||||
iteration_count => IterationCount},
|
||||
{ok, State}.
|
||||
|
||||
update(_ChainID, _Authenticator, _Config, _State) ->
|
||||
{error, update_not_suppored}.
|
||||
|
||||
authenticate(#{auth_method := AuthMethod,
|
||||
auth_data := AuthData,
|
||||
auth_cache := AuthCache}, State) ->
|
||||
case ensure_auth_method(AuthMethod, State) of
|
||||
true ->
|
||||
case AuthCache of
|
||||
#{next_step := client_final} ->
|
||||
check_client_final_message(AuthData, AuthCache, State);
|
||||
_ ->
|
||||
check_client_first_message(AuthData, AuthCache, State)
|
||||
end;
|
||||
false ->
|
||||
ignore
|
||||
end;
|
||||
authenticate(_Credential, _State) ->
|
||||
ignore.
|
||||
|
||||
destroy(#{user_group := UserGroup}) ->
|
||||
trans(
|
||||
fun() ->
|
||||
MatchSpec = [{{scram_user_credentail, {UserGroup, '_'}, '_', '_', '_'}, [], ['$_']}],
|
||||
ok = lists:foreach(fun(UserCredential) ->
|
||||
mnesia:delete_object(?TAB, UserCredential, write)
|
||||
end, mnesia:select(?TAB, MatchSpec, write))
|
||||
end).
|
||||
|
||||
%% TODO: binary to atom
|
||||
add_user(#{<<"user_id">> := UserID,
|
||||
<<"password">> := Password}, #{user_group := UserGroup} = State) ->
|
||||
trans(
|
||||
fun() ->
|
||||
case mnesia:read(?TAB, {UserGroup, UserID}, write) of
|
||||
[] ->
|
||||
add_user(UserID, Password, State),
|
||||
{ok, #{user_id => UserID}};
|
||||
[_] ->
|
||||
{error, already_exist}
|
||||
end
|
||||
end).
|
||||
|
||||
delete_user(UserID, #{user_group := UserGroup}) ->
|
||||
trans(
|
||||
fun() ->
|
||||
case mnesia:read(?TAB, {UserGroup, UserID}, write) of
|
||||
[] ->
|
||||
{error, not_found};
|
||||
[_] ->
|
||||
mnesia:delete(?TAB, {UserGroup, UserID}, write)
|
||||
end
|
||||
end).
|
||||
|
||||
update_user(UserID, #{<<"password">> := Password},
|
||||
#{user_group := UserGroup} = State) ->
|
||||
trans(
|
||||
fun() ->
|
||||
case mnesia:read(?TAB, {UserGroup, UserID}, write) of
|
||||
[] ->
|
||||
{error, not_found};
|
||||
[_] ->
|
||||
add_user(UserID, Password, State),
|
||||
{ok, #{user_id => UserID}}
|
||||
end
|
||||
end).
|
||||
|
||||
lookup_user(UserID, #{user_group := UserGroup}) ->
|
||||
case mnesia:dirty_read(?TAB, {UserGroup, UserID}) of
|
||||
[#scram_user_credentail{user_id = {_, UserID}}] ->
|
||||
{ok, #{user_id => UserID}};
|
||||
[] ->
|
||||
{error, not_found}
|
||||
end.
|
||||
|
||||
%% TODO: Support Pagination
|
||||
list_users(#{user_group := UserGroup}) ->
|
||||
Users = [#{user_id => UserID} ||
|
||||
#scram_user_credentail{user_id = {UserGroup0, UserID}} <- ets:tab2list(?TAB), UserGroup0 =:= UserGroup],
|
||||
{ok, Users}.
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
%% Internal functions
|
||||
%%------------------------------------------------------------------------------
|
||||
|
||||
ensure_auth_method('SCRAM-SHA-256', #{algorithm := sha256}) ->
|
||||
true;
|
||||
ensure_auth_method('SCRAM-SHA-512', #{algorithm := sha512}) ->
|
||||
true;
|
||||
ensure_auth_method(_, _) ->
|
||||
false.
|
||||
|
||||
check_client_first_message(Bin, _Cache, #{iteration_count := IterationCount} = State) ->
|
||||
LookupFun = fun(Username) ->
|
||||
lookup_user2(Username, State)
|
||||
end,
|
||||
case esasl_scram:check_client_first_message(
|
||||
Bin,
|
||||
#{iteration_count => IterationCount,
|
||||
lookup => LookupFun}
|
||||
) of
|
||||
{cotinue, ServerFirstMessage, Cache} ->
|
||||
{cotinue, ServerFirstMessage, Cache};
|
||||
{error, _Reason} ->
|
||||
{error, not_authorized}
|
||||
end.
|
||||
|
||||
check_client_final_message(Bin, Cache, #{algorithm := Alg}) ->
|
||||
case esasl_scram:check_client_final_message(
|
||||
Bin,
|
||||
Cache#{algorithm => Alg}
|
||||
) of
|
||||
{ok, ServerFinalMessage} ->
|
||||
{ok, ServerFinalMessage};
|
||||
{error, _Reason} ->
|
||||
{error, not_authorized}
|
||||
end.
|
||||
|
||||
add_user(UserID, Password, State) ->
|
||||
UserCredential = esasl_scram:generate_user_credential(UserID, Password, State),
|
||||
mnesia:write(?TAB, UserCredential, write).
|
||||
|
||||
lookup_user2(UserID, #{user_group := UserGroup}) ->
|
||||
case mnesia:dirty_read(?TAB, {UserGroup, UserID}) of
|
||||
[#scram_user_credentail{} = UserCredential] ->
|
||||
{ok, UserCredential};
|
||||
[] ->
|
||||
{error, not_found}
|
||||
end.
|
||||
|
||||
%% TODO: Move to emqx_authn_utils.erl
|
||||
trans(Fun) ->
|
||||
trans(Fun, []).
|
||||
|
||||
trans(Fun, Args) ->
|
||||
case ekka_mnesia:transaction(?AUTH_SHARD, Fun, Args) of
|
||||
{atomic, Res} -> Res;
|
||||
{aborted, Reason} -> {error, Reason}
|
||||
end.
|
|
@ -46,10 +46,9 @@
|
|||
structs() -> [""].
|
||||
|
||||
fields("") ->
|
||||
[ {config, #{type => hoconsc:union(
|
||||
[ hoconsc:ref(?MODULE, get)
|
||||
, hoconsc:ref(?MODULE, post)
|
||||
])}}
|
||||
[ {config, {union, [ hoconsc:t(get)
|
||||
, hoconsc:t(post)
|
||||
]}}
|
||||
];
|
||||
|
||||
fields(get) ->
|
||||
|
@ -64,7 +63,8 @@ fields(post) ->
|
|||
] ++ common_fields().
|
||||
|
||||
common_fields() ->
|
||||
[ {url, fun url/1}
|
||||
[ {server_type, {enum, ['http-server']}}
|
||||
, {url, fun url/1}
|
||||
, {accept, fun accept/1}
|
||||
, {headers, fun headers/1}
|
||||
, {form_data, fun form_data/1}
|
||||
|
@ -142,10 +142,12 @@ update(_ChainID, _AuthenticatorName, Config, #{resource_id := ResourceID} = Stat
|
|||
{error, Reason} -> {error, Reason}
|
||||
end.
|
||||
|
||||
authenticate(ClientInfo, #{resource_id := ResourceID,
|
||||
authenticate(#{auth_method := _}, _) ->
|
||||
ignore;
|
||||
authenticate(Credential, #{resource_id := ResourceID,
|
||||
method := Method,
|
||||
request_timeout := RequestTimeout} = State) ->
|
||||
Request = generate_request(ClientInfo, State),
|
||||
Request = generate_request(Credential, State),
|
||||
case emqx_resource:query(ResourceID, {Method, Request, RequestTimeout}) of
|
||||
{ok, 204, _Headers} -> ok;
|
||||
{ok, 200, Headers, Body} ->
|
||||
|
@ -154,8 +156,8 @@ authenticate(ClientInfo, #{resource_id := ResourceID,
|
|||
{ok, _NBody} ->
|
||||
%% TODO: Return by user property
|
||||
ok;
|
||||
{error, Reason} ->
|
||||
{stop, Reason}
|
||||
{error, _Reason} ->
|
||||
ok
|
||||
end;
|
||||
{error, _Reason} ->
|
||||
ignore
|
||||
|
@ -208,13 +210,13 @@ generate_base_url(#{scheme := Scheme,
|
|||
port := Port}) ->
|
||||
iolist_to_binary(io_lib:format("~p://~s:~p", [Scheme, Host, Port])).
|
||||
|
||||
generate_request(ClientInfo, #{method := Method,
|
||||
generate_request(Credential, #{method := Method,
|
||||
path := Path,
|
||||
base_query := BaseQuery,
|
||||
content_type := ContentType,
|
||||
headers := Headers,
|
||||
form_data := FormData0}) ->
|
||||
FormData = replace_placeholders(FormData0, ClientInfo),
|
||||
FormData = replace_placeholders(FormData0, Credential),
|
||||
case Method of
|
||||
get ->
|
||||
NPath = append_query(Path, BaseQuery ++ FormData),
|
||||
|
@ -225,9 +227,9 @@ generate_request(ClientInfo, #{method := Method,
|
|||
{NPath, Headers, Body}
|
||||
end.
|
||||
|
||||
replace_placeholders(FormData0, ClientInfo) ->
|
||||
replace_placeholders(FormData0, Credential) ->
|
||||
FormData = lists:map(fun({K, V0}) ->
|
||||
case replace_placeholder(V0, ClientInfo) of
|
||||
case replace_placeholder(V0, Credential) of
|
||||
undefined -> {K, undefined};
|
||||
V -> {K, bin(V)}
|
||||
end
|
||||
|
@ -236,16 +238,16 @@ replace_placeholders(FormData0, ClientInfo) ->
|
|||
V =/= undefined
|
||||
end, FormData).
|
||||
|
||||
replace_placeholder(<<"${mqtt-username}">>, ClientInfo) ->
|
||||
maps:get(username, ClientInfo, undefined);
|
||||
replace_placeholder(<<"${mqtt-clientid}">>, ClientInfo) ->
|
||||
maps:get(clientid, ClientInfo, undefined);
|
||||
replace_placeholder(<<"${ip-address}">>, ClientInfo) ->
|
||||
maps:get(peerhost, ClientInfo, undefined);
|
||||
replace_placeholder(<<"${cert-subject}">>, ClientInfo) ->
|
||||
maps:get(dn, ClientInfo, undefined);
|
||||
replace_placeholder(<<"${cert-common-name}">>, ClientInfo) ->
|
||||
maps:get(cn, ClientInfo, undefined);
|
||||
replace_placeholder(<<"${mqtt-username}">>, Credential) ->
|
||||
maps:get(username, Credential, undefined);
|
||||
replace_placeholder(<<"${mqtt-clientid}">>, Credential) ->
|
||||
maps:get(clientid, 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.
|
||||
|
||||
|
|
|
@ -132,10 +132,7 @@ handle_options(#{endpoint := Endpoint,
|
|||
refresh_interval => limit_refresh_interval(RefreshInterval0),
|
||||
ssl_opts => maps:to_list(SSLOpts),
|
||||
jwks => [],
|
||||
request_id => undefined};
|
||||
|
||||
handle_options(#{enable_ssl := false} = Opts) ->
|
||||
handle_options(Opts#{ssl_opts => #{}}).
|
||||
request_id => undefined}.
|
||||
|
||||
refresh_jwks(#{endpoint := Endpoint,
|
||||
ssl_opts := SSLOpts} = State) ->
|
||||
|
|
|
@ -35,21 +35,14 @@
|
|||
%% Hocon Schema
|
||||
%%------------------------------------------------------------------------------
|
||||
|
||||
structs() -> [config].
|
||||
structs() -> [""].
|
||||
|
||||
fields("") ->
|
||||
[{config, {union, [ hoconsc:t('hmac-based')
|
||||
, hoconsc:t('public-key')
|
||||
, hoconsc:t('jwks')
|
||||
, hoconsc:t('jwks-using-ssl')
|
||||
]}}];
|
||||
|
||||
fields(config) ->
|
||||
[{union, [ hoconsc:t('hmac-based')
|
||||
, hoconsc:t('public-key')
|
||||
, hoconsc:t('jwks')
|
||||
, hoconsc:t('jwks-using-ssl')
|
||||
]}];
|
||||
[ {config, {union, [ hoconsc:t('hmac-based')
|
||||
, hoconsc:t('public-key')
|
||||
, hoconsc:t('jwks')
|
||||
]}}
|
||||
];
|
||||
|
||||
fields('hmac-based') ->
|
||||
[ {use_jwks, {enum, [false]}}
|
||||
|
@ -67,35 +60,35 @@ fields('public-key') ->
|
|||
];
|
||||
|
||||
fields('jwks') ->
|
||||
[ {enable_ssl, {enum, [false]}}
|
||||
] ++ jwks_fields();
|
||||
[ {use_jwks, {enum, [true]}}
|
||||
, {endpoint, fun endpoint/1}
|
||||
, {refresh_interval, fun refresh_interval/1}
|
||||
, {verify_claims, fun verify_claims/1}
|
||||
, {ssl, #{type => hoconsc:union(
|
||||
[ hoconsc:ref(?MODULE, ssl_enable)
|
||||
, hoconsc:ref(?MODULE, ssl_disable)
|
||||
]),
|
||||
default => #{<<"enable">> => false}}}
|
||||
];
|
||||
|
||||
fields('jwks-using-ssl') ->
|
||||
[ {enable_ssl, {enum, [true]}}
|
||||
, {ssl_opts, fun ssl_opts/1}
|
||||
] ++ jwks_fields();
|
||||
|
||||
fields(ssl_opts) ->
|
||||
[ {cacertfile, fun cacertfile/1}
|
||||
fields(ssl_enable) ->
|
||||
[ {enable, #{type => true}}
|
||||
, {cacertfile, fun cacertfile/1}
|
||||
, {certfile, fun certfile/1}
|
||||
, {keyfile, fun keyfile/1}
|
||||
, {verify, fun verify/1}
|
||||
, {server_name_indication, fun server_name_indication/1}
|
||||
];
|
||||
|
||||
fields(ssl_disable) ->
|
||||
[ {enable, #{type => false}} ];
|
||||
|
||||
fields(claim) ->
|
||||
[ {"$name", fun expected_claim_value/1} ].
|
||||
|
||||
validations() ->
|
||||
[ {check_verify_claims, fun check_verify_claims/1} ].
|
||||
|
||||
jwks_fields() ->
|
||||
[ {use_jwks, {enum, [true]}}
|
||||
, {endpoint, fun endpoint/1}
|
||||
, {refresh_interval, fun refresh_interval/1}
|
||||
, {verify_claims, fun verify_claims/1}
|
||||
].
|
||||
|
||||
secret(type) -> string();
|
||||
secret(_) -> undefined.
|
||||
|
||||
|
@ -109,9 +102,9 @@ certificate(_) -> undefined.
|
|||
endpoint(type) -> string();
|
||||
endpoint(_) -> undefined.
|
||||
|
||||
ssl_opts(type) -> hoconsc:t(hoconsc:ref(ssl_opts));
|
||||
ssl_opts(default) -> [];
|
||||
ssl_opts(_) -> undefined.
|
||||
% ssl_opts(type) -> hoconsc:t(hoconsc:ref(ssl_opts));
|
||||
% ssl_opts(default) -> [];
|
||||
% ssl_opts(_) -> undefined.
|
||||
|
||||
refresh_interval(type) -> integer();
|
||||
refresh_interval(default) -> 300;
|
||||
|
@ -169,7 +162,9 @@ update(_ChainID, _AuthenticatorName, #{use_jwks := true} = Config, #{jwk := Conn
|
|||
update(_ChainID, _AuthenticatorName, #{use_jwks := true} = Config, _) ->
|
||||
create(Config).
|
||||
|
||||
authenticate(ClientInfo = #{password := JWT}, #{jwk := JWK,
|
||||
authenticate(#{auth_method := _}, _) ->
|
||||
ignore;
|
||||
authenticate(Credential = #{password := JWT}, #{jwk := JWK,
|
||||
verify_claims := VerifyClaims0}) ->
|
||||
JWKs = case erlang:is_pid(JWK) of
|
||||
false ->
|
||||
|
@ -178,11 +173,11 @@ authenticate(ClientInfo = #{password := JWT}, #{jwk := JWK,
|
|||
{ok, JWKs0} = emqx_authn_jwks_connector:get_jwks(JWK),
|
||||
JWKs0
|
||||
end,
|
||||
VerifyClaims = replace_placeholder(VerifyClaims0, ClientInfo),
|
||||
VerifyClaims = replace_placeholder(VerifyClaims0, Credential),
|
||||
case verify(JWT, JWKs, VerifyClaims) of
|
||||
ok -> ok;
|
||||
{error, invalid_signature} -> ignore;
|
||||
{error, {claims, _}} -> {stop, bad_password}
|
||||
{error, {claims, _}} -> {error, bad_username_or_password}
|
||||
end.
|
||||
|
||||
destroy(#{jwk := Connector}) when is_pid(Connector) ->
|
||||
|
@ -222,8 +217,13 @@ create2(#{use_jwks := false,
|
|||
verify_claims => VerifyClaims}};
|
||||
|
||||
create2(#{use_jwks := true,
|
||||
verify_claims := VerifyClaims} = Config) ->
|
||||
case emqx_authn_jwks_connector:start_link(Config) of
|
||||
verify_claims := VerifyClaims,
|
||||
ssl := #{enable := Enable} = SSL} = Config) ->
|
||||
SSLOpts = case Enable of
|
||||
true -> maps:without(enable, SSL);
|
||||
false -> #{}
|
||||
end,
|
||||
case emqx_authn_jwks_connector:start_link(Config#{ssl_opts => SSLOpts}) of
|
||||
{ok, Connector} ->
|
||||
{ok, #{jwk => Connector,
|
||||
verify_claims => VerifyClaims}};
|
||||
|
|
|
@ -55,7 +55,7 @@
|
|||
-boot_mnesia({mnesia, [boot]}).
|
||||
-copy_mnesia({mnesia, [copy]}).
|
||||
|
||||
-define(TAB, mnesia_basic_auth).
|
||||
-define(TAB, ?MODULE).
|
||||
|
||||
-rlog_shard({?AUTH_SHARD, ?TAB}).
|
||||
%%------------------------------------------------------------------------------
|
||||
|
@ -81,7 +81,8 @@ mnesia(copy) ->
|
|||
structs() -> [config].
|
||||
|
||||
fields(config) ->
|
||||
[ {user_id_type, fun user_id_type/1}
|
||||
[ {server_type, {enum, ['built-in-database']}}
|
||||
, {user_id_type, fun user_id_type/1}
|
||||
, {password_hash_algorithm, fun password_hash_algorithm/1}
|
||||
];
|
||||
|
||||
|
@ -95,11 +96,11 @@ fields(other_algorithms) ->
|
|||
].
|
||||
|
||||
user_id_type(type) -> user_id_type();
|
||||
user_id_type(default) -> clientid;
|
||||
user_id_type(default) -> username;
|
||||
user_id_type(_) -> undefined.
|
||||
|
||||
password_hash_algorithm(type) -> {union, [hoconsc:ref(bcrypt), hoconsc:ref(other_algorithms)]};
|
||||
password_hash_algorithm(default) -> sha256;
|
||||
password_hash_algorithm(default) -> #{<<"name">> => sha256};
|
||||
password_hash_algorithm(_) -> undefined.
|
||||
|
||||
salt_rounds(type) -> integer();
|
||||
|
@ -130,11 +131,13 @@ create(ChainID, AuthenticatorName, #{user_id_type := Type,
|
|||
update(ChainID, AuthenticatorName, Config, _State) ->
|
||||
create(ChainID, AuthenticatorName, Config).
|
||||
|
||||
authenticate(ClientInfo = #{password := Password},
|
||||
authenticate(#{auth_method := _}, _) ->
|
||||
ignore;
|
||||
authenticate(#{password := Password} = Credential,
|
||||
#{user_group := UserGroup,
|
||||
user_id_type := Type,
|
||||
password_hash_algorithm := Algorithm}) ->
|
||||
UserID = get_user_identity(ClientInfo, Type),
|
||||
UserID = get_user_identity(Credential, Type),
|
||||
case mnesia:dirty_read(?TAB, {UserGroup, UserID}) of
|
||||
[] ->
|
||||
ignore;
|
||||
|
@ -145,7 +148,7 @@ authenticate(ClientInfo = #{password := Password},
|
|||
end,
|
||||
case PasswordHash =:= hash(Algorithm, Password, Salt) of
|
||||
true -> ok;
|
||||
false -> {stop, bad_password}
|
||||
false -> {error, bad_username_or_password}
|
||||
end
|
||||
end.
|
||||
|
||||
|
@ -330,8 +333,7 @@ gen_salt(#{password_hash_algorithm := bcrypt,
|
|||
{ok, Salt} = bcrypt:gen_salt(Rounds),
|
||||
Salt;
|
||||
gen_salt(_) ->
|
||||
<<X:128/big-unsigned-integer>> = crypto:strong_rand_bytes(16),
|
||||
iolist_to_binary(io_lib:format("~32.16.0b", [X])).
|
||||
emqx_authn_utils:gen_salt().
|
||||
|
||||
hash(bcrypt, Password, Salt) ->
|
||||
{ok, Hash} = bcrypt:hashpw(Password, Salt),
|
||||
|
@ -343,10 +345,10 @@ insert_user(UserGroup, UserID, PasswordHash) ->
|
|||
insert_user(UserGroup, UserID, PasswordHash, <<>>).
|
||||
|
||||
insert_user(UserGroup, UserID, PasswordHash, Salt) ->
|
||||
Credential = #user_info{user_id = {UserGroup, UserID},
|
||||
password_hash = PasswordHash,
|
||||
salt = Salt},
|
||||
mnesia:write(?TAB, Credential, write).
|
||||
UserInfo = #user_info{user_id = {UserGroup, UserID},
|
||||
password_hash = PasswordHash,
|
||||
salt = Salt},
|
||||
mnesia:write(?TAB, UserInfo, write).
|
||||
|
||||
delete_user2(UserInfo) ->
|
||||
mnesia:delete_object(?TAB, UserInfo, write).
|
||||
|
|
|
@ -36,7 +36,8 @@
|
|||
structs() -> [config].
|
||||
|
||||
fields(config) ->
|
||||
[ {password_hash_algorithm, fun password_hash_algorithm/1}
|
||||
[ {server_type, {enum, [mysql]}}
|
||||
, {password_hash_algorithm, fun password_hash_algorithm/1}
|
||||
, {salt_position, {enum, [prefix, suffix]}}
|
||||
, {query, fun query/1}
|
||||
, {query_timeout, fun query_timeout/1}
|
||||
|
@ -81,12 +82,14 @@ update(_ChainID, _AuthenticatorName, Config, #{resource_id := ResourceID} = Stat
|
|||
{error, Reason} -> {error, Reason}
|
||||
end.
|
||||
|
||||
authenticate(#{password := Password} = ClientInfo,
|
||||
authenticate(#{auth_method := _}, _) ->
|
||||
ignore;
|
||||
authenticate(#{password := Password} = Credential,
|
||||
#{resource_id := ResourceID,
|
||||
placeholders := PlaceHolders,
|
||||
query := Query,
|
||||
query_timeout := Timeout} = State) ->
|
||||
Params = emqx_authn_utils:replace_placeholder(PlaceHolders, ClientInfo),
|
||||
Params = emqx_authn_utils:replace_placeholder(PlaceHolders, Credential),
|
||||
case emqx_resource:query(ResourceID, {sql, Query, Params, Timeout}) of
|
||||
{ok, _Columns, []} -> ignore;
|
||||
{ok, Columns, Rows} ->
|
||||
|
@ -106,14 +109,14 @@ destroy(#{resource_id := ResourceID}) ->
|
|||
%%------------------------------------------------------------------------------
|
||||
|
||||
check_password(undefined, _Algorithm, _Selected) ->
|
||||
{stop, bad_password};
|
||||
{error, bad_username_or_password};
|
||||
check_password(Password,
|
||||
#{password_hash := Hash},
|
||||
#{password_hash_algorithm := bcrypt}) ->
|
||||
{ok, Hash0} = bcrypt:hashpw(Password, Hash),
|
||||
case list_to_binary(Hash0) =:= Hash of
|
||||
true -> ok;
|
||||
false -> {stop, bad_password}
|
||||
false -> {error, bad_username_or_password}
|
||||
end;
|
||||
check_password(Password,
|
||||
#{password_hash := Hash} = Selected,
|
||||
|
@ -126,7 +129,7 @@ check_password(Password,
|
|||
end,
|
||||
case Hash0 =:= Hash of
|
||||
true -> ok;
|
||||
false -> {stop, bad_password}
|
||||
false -> {error, bad_username_or_password}
|
||||
end.
|
||||
|
||||
%% TODO: Support prepare
|
||||
|
|
|
@ -36,9 +36,10 @@
|
|||
structs() -> [config].
|
||||
|
||||
fields(config) ->
|
||||
[ {password_hash_algorithm, fun password_hash_algorithm/1}
|
||||
, {salt_position, {enum, [prefix, suffix]}}
|
||||
, {query, fun query/1}
|
||||
[ {server_type, {enum, [pgsql]}}
|
||||
, {password_hash_algorithm, fun password_hash_algorithm/1}
|
||||
, {salt_position, {enum, [prefix, suffix]}}
|
||||
, {query, fun query/1}
|
||||
] ++ emqx_connector_schema_lib:relational_db_fields()
|
||||
++ emqx_connector_schema_lib:ssl_fields().
|
||||
|
||||
|
@ -75,11 +76,13 @@ update(_ChainID, _ServiceName, Config, #{resource_id := ResourceID} = State) ->
|
|||
{error, Reason} -> {error, Reason}
|
||||
end.
|
||||
|
||||
authenticate(#{password := Password} = ClientInfo,
|
||||
authenticate(#{auth_method := _}, _) ->
|
||||
ignore;
|
||||
authenticate(#{password := Password} = Credential,
|
||||
#{resource_id := ResourceID,
|
||||
query := Query,
|
||||
placeholders := PlaceHolders} = State) ->
|
||||
Params = emqx_authn_utils:replace_placeholder(PlaceHolders, ClientInfo),
|
||||
Params = emqx_authn_utils:replace_placeholder(PlaceHolders, Credential),
|
||||
case emqx_resource:query(ResourceID, {sql, Query, Params}) of
|
||||
{ok, _Columns, []} -> ignore;
|
||||
{ok, Columns, Rows} ->
|
||||
|
@ -99,14 +102,14 @@ destroy(#{resource_id := ResourceID}) ->
|
|||
%%------------------------------------------------------------------------------
|
||||
|
||||
check_password(undefined, _Algorithm, _Selected) ->
|
||||
{stop, bad_password};
|
||||
{error, bad_username_or_password};
|
||||
check_password(Password,
|
||||
#{password_hash := Hash},
|
||||
#{password_hash_algorithm := bcrypt}) ->
|
||||
{ok, Hash0} = bcrypt:hashpw(Password, Hash),
|
||||
case list_to_binary(Hash0) =:= Hash of
|
||||
true -> ok;
|
||||
false -> {stop, bad_password}
|
||||
false -> {error, bad_username_or_password}
|
||||
end;
|
||||
check_password(Password,
|
||||
#{password_hash := Hash} = Selected,
|
||||
|
@ -119,7 +122,7 @@ check_password(Password,
|
|||
end,
|
||||
case Hash0 =:= Hash of
|
||||
true -> ok;
|
||||
false -> {stop, bad_password}
|
||||
false -> {error, bad_username_or_password}
|
||||
end.
|
||||
|
||||
%% TODO: Support prepare
|
||||
|
|
|
@ -22,6 +22,8 @@
|
|||
-include_lib("common_test/include/ct.hrl").
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
|
||||
-include("emqx_authn.hrl").
|
||||
|
||||
-define(AUTH, emqx_authn).
|
||||
|
||||
all() ->
|
||||
|
@ -40,16 +42,17 @@ end_per_suite(_) ->
|
|||
set_special_configs(emqx_authn) ->
|
||||
application:set_env(emqx, plugins_etc_dir,
|
||||
emqx_ct_helpers:deps_path(emqx_authn, "test")),
|
||||
Conf = #{<<"authn">> => #{<<"chains">> => [], <<"bindings">> => []}},
|
||||
Conf = #{<<"emqx_authn">> => #{<<"authenticators">> => []}},
|
||||
ok = file:write_file(filename:join(emqx:get_env(plugins_etc_dir), 'emqx_authn.conf'), jsx:encode(Conf)),
|
||||
ok;
|
||||
set_special_configs(_App) ->
|
||||
ok.
|
||||
|
||||
t_chain(_) ->
|
||||
?assertMatch({ok, #{id := ?CHAIN, authenticators := []}}, ?AUTH:lookup_chain(?CHAIN)),
|
||||
|
||||
ChainID = <<"mychain">>,
|
||||
Chain = #{id => ChainID,
|
||||
type => simple},
|
||||
Chain = #{id => ChainID},
|
||||
?assertMatch({ok, #{id := ChainID, authenticators := []}}, ?AUTH:create_chain(Chain)),
|
||||
?assertEqual({error, {already_exists, {chain, ChainID}}}, ?AUTH:create_chain(Chain)),
|
||||
?assertMatch({ok, #{id := ChainID, authenticators := []}}, ?AUTH:lookup_chain(ChainID)),
|
||||
|
@ -57,86 +60,37 @@ t_chain(_) ->
|
|||
?assertMatch({error, {not_found, {chain, ChainID}}}, ?AUTH:lookup_chain(ChainID)),
|
||||
ok.
|
||||
|
||||
t_binding(_) ->
|
||||
Listener1 = <<"listener1">>,
|
||||
Listener2 = <<"listener2">>,
|
||||
ChainID = <<"mychain">>,
|
||||
|
||||
?assertEqual({error, {not_found, {chain, ChainID}}}, ?AUTH:bind(ChainID, [Listener1])),
|
||||
|
||||
Chain = #{id => ChainID,
|
||||
type => simple},
|
||||
?assertMatch({ok, #{id := ChainID, authenticators := []}}, ?AUTH:create_chain(Chain)),
|
||||
|
||||
?assertEqual(ok, ?AUTH:bind(ChainID, [Listener1])),
|
||||
?assertEqual(ok, ?AUTH:bind(ChainID, [Listener2])),
|
||||
?assertEqual({error, {already_bound, [Listener1]}}, ?AUTH:bind(ChainID, [Listener1])),
|
||||
{ok, #{listeners := Listeners}} = ?AUTH:list_bindings(ChainID),
|
||||
?assertEqual(2, length(Listeners)),
|
||||
?assertMatch({ok, #{simple := ChainID}}, ?AUTH:list_bound_chains(Listener1)),
|
||||
|
||||
?assertEqual(ok, ?AUTH:unbind(ChainID, [Listener1])),
|
||||
?assertEqual(ok, ?AUTH:unbind(ChainID, [Listener2])),
|
||||
?assertEqual({error, {not_found, [Listener1]}}, ?AUTH:unbind(ChainID, [Listener1])),
|
||||
|
||||
?assertEqual(ok, ?AUTH:delete_chain(ChainID)),
|
||||
ok.
|
||||
|
||||
t_binding2(_) ->
|
||||
ChainID = <<"mychain">>,
|
||||
Chain = #{id => ChainID,
|
||||
type => simple},
|
||||
?assertMatch({ok, #{id := ChainID, authenticators := []}}, ?AUTH:create_chain(Chain)),
|
||||
|
||||
Listener1 = <<"listener1">>,
|
||||
Listener2 = <<"listener2">>,
|
||||
|
||||
?assertEqual(ok, ?AUTH:bind(ChainID, [Listener1, Listener2])),
|
||||
{ok, #{listeners := Listeners}} = ?AUTH:list_bindings(ChainID),
|
||||
?assertEqual(2, length(Listeners)),
|
||||
?assertEqual(ok, ?AUTH:unbind(ChainID, [Listener1, Listener2])),
|
||||
?assertMatch({ok, #{listeners := []}}, ?AUTH:list_bindings(ChainID)),
|
||||
|
||||
?assertEqual(ok, ?AUTH:delete_chain(ChainID)),
|
||||
ok.
|
||||
|
||||
t_authenticator(_) ->
|
||||
ChainID = <<"mychain">>,
|
||||
Chain = #{id => ChainID,
|
||||
type => simple},
|
||||
?assertMatch({ok, #{id := ChainID, authenticators := []}}, ?AUTH:create_chain(Chain)),
|
||||
?assertMatch({ok, #{id := ChainID, authenticators := []}}, ?AUTH:lookup_chain(ChainID)),
|
||||
|
||||
AuthenticatorName1 = <<"myauthenticator1">>,
|
||||
AuthenticatorConfig1 = #{name => AuthenticatorName1,
|
||||
type => 'built-in-database',
|
||||
mechanism => 'password-based',
|
||||
config => #{
|
||||
server_type => 'built-in-database',
|
||||
user_id_type => username,
|
||||
password_hash_algorithm => #{
|
||||
name => sha256
|
||||
}}},
|
||||
?assertEqual({ok, AuthenticatorConfig1}, ?AUTH:create_authenticator(ChainID, AuthenticatorConfig1)),
|
||||
?assertEqual({ok, AuthenticatorConfig1}, ?AUTH:lookup_authenticator(ChainID, AuthenticatorName1)),
|
||||
?assertEqual({ok, [AuthenticatorConfig1]}, ?AUTH:list_authenticators(ChainID)),
|
||||
?assertEqual({error, {already_exists, {authenticator, AuthenticatorName1}}}, ?AUTH:create_authenticator(ChainID, AuthenticatorConfig1)),
|
||||
?assertEqual({ok, AuthenticatorConfig1}, ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig1)),
|
||||
?assertEqual({ok, AuthenticatorConfig1}, ?AUTH:lookup_authenticator(?CHAIN, AuthenticatorName1)),
|
||||
?assertEqual({ok, [AuthenticatorConfig1]}, ?AUTH:list_authenticators(?CHAIN)),
|
||||
?assertEqual({error, {already_exists, {authenticator, AuthenticatorName1}}}, ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig1)),
|
||||
|
||||
AuthenticatorName2 = <<"myauthenticator2">>,
|
||||
AuthenticatorConfig2 = AuthenticatorConfig1#{name => AuthenticatorName2},
|
||||
?assertEqual({ok, AuthenticatorConfig2}, ?AUTH:create_authenticator(ChainID, AuthenticatorConfig2)),
|
||||
?assertMatch({ok, #{id := ChainID, authenticators := [AuthenticatorConfig1, AuthenticatorConfig2]}}, ?AUTH:lookup_chain(ChainID)),
|
||||
?assertEqual({ok, AuthenticatorConfig2}, ?AUTH:lookup_authenticator(ChainID, AuthenticatorName2)),
|
||||
?assertEqual({ok, [AuthenticatorConfig1, AuthenticatorConfig2]}, ?AUTH:list_authenticators(ChainID)),
|
||||
?assertEqual({ok, AuthenticatorConfig2}, ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig2)),
|
||||
?assertMatch({ok, #{id := ?CHAIN, authenticators := [AuthenticatorConfig1, AuthenticatorConfig2]}}, ?AUTH:lookup_chain(?CHAIN)),
|
||||
?assertEqual({ok, AuthenticatorConfig2}, ?AUTH:lookup_authenticator(?CHAIN, AuthenticatorName2)),
|
||||
?assertEqual({ok, [AuthenticatorConfig1, AuthenticatorConfig2]}, ?AUTH:list_authenticators(?CHAIN)),
|
||||
|
||||
?assertEqual(ok, ?AUTH:move_authenticator_to_the_front(ChainID, AuthenticatorName2)),
|
||||
?assertEqual({ok, [AuthenticatorConfig2, AuthenticatorConfig1]}, ?AUTH:list_authenticators(ChainID)),
|
||||
?assertEqual(ok, ?AUTH:move_authenticator_to_the_end(ChainID, AuthenticatorName2)),
|
||||
?assertEqual({ok, [AuthenticatorConfig1, AuthenticatorConfig2]}, ?AUTH:list_authenticators(ChainID)),
|
||||
?assertEqual(ok, ?AUTH:move_authenticator_to_the_nth(ChainID, AuthenticatorName2, 1)),
|
||||
?assertEqual({ok, [AuthenticatorConfig2, AuthenticatorConfig1]}, ?AUTH:list_authenticators(ChainID)),
|
||||
?assertEqual({error, out_of_range}, ?AUTH:move_authenticator_to_the_nth(ChainID, AuthenticatorName2, 3)),
|
||||
?assertEqual({error, out_of_range}, ?AUTH:move_authenticator_to_the_nth(ChainID, AuthenticatorName2, 0)),
|
||||
?assertEqual(ok, ?AUTH:delete_authenticator(ChainID, AuthenticatorName1)),
|
||||
?assertEqual(ok, ?AUTH:delete_authenticator(ChainID, AuthenticatorName2)),
|
||||
?assertEqual({ok, []}, ?AUTH:list_authenticators(ChainID)),
|
||||
?assertEqual(ok, ?AUTH:delete_chain(ChainID)),
|
||||
?assertEqual(ok, ?AUTH:move_authenticator_to_the_front(?CHAIN, AuthenticatorName2)),
|
||||
?assertEqual({ok, [AuthenticatorConfig2, AuthenticatorConfig1]}, ?AUTH:list_authenticators(?CHAIN)),
|
||||
?assertEqual(ok, ?AUTH:move_authenticator_to_the_end(?CHAIN, AuthenticatorName2)),
|
||||
?assertEqual({ok, [AuthenticatorConfig1, AuthenticatorConfig2]}, ?AUTH:list_authenticators(?CHAIN)),
|
||||
?assertEqual(ok, ?AUTH:move_authenticator_to_the_nth(?CHAIN, AuthenticatorName2, 1)),
|
||||
?assertEqual({ok, [AuthenticatorConfig2, AuthenticatorConfig1]}, ?AUTH:list_authenticators(?CHAIN)),
|
||||
?assertEqual({error, out_of_range}, ?AUTH:move_authenticator_to_the_nth(?CHAIN, AuthenticatorName2, 3)),
|
||||
?assertEqual({error, out_of_range}, ?AUTH:move_authenticator_to_the_nth(?CHAIN, AuthenticatorName2, 0)),
|
||||
?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, AuthenticatorName1)),
|
||||
?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, AuthenticatorName2)),
|
||||
?assertEqual({ok, []}, ?AUTH:list_authenticators(?CHAIN)),
|
||||
ok.
|
||||
|
|
|
@ -22,6 +22,8 @@
|
|||
-include_lib("common_test/include/ct.hrl").
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
|
||||
-include("emqx_authn.hrl").
|
||||
|
||||
-define(AUTH, emqx_authn).
|
||||
|
||||
all() ->
|
||||
|
@ -39,18 +41,13 @@ end_per_suite(_) ->
|
|||
set_special_configs(emqx_authn) ->
|
||||
application:set_env(emqx, plugins_etc_dir,
|
||||
emqx_ct_helpers:deps_path(emqx_authn, "test")),
|
||||
Conf = #{<<"authn">> => #{<<"chains">> => [], <<"bindings">> => []}},
|
||||
Conf = #{<<"emqx_authn">> => #{<<"authenticators">> => []}},
|
||||
ok = file:write_file(filename:join(emqx:get_env(plugins_etc_dir), 'emqx_authn.conf'), jsx:encode(Conf)),
|
||||
ok;
|
||||
set_special_configs(_App) ->
|
||||
ok.
|
||||
|
||||
t_jwt_authenticator(_) ->
|
||||
ChainID = <<"mychain">>,
|
||||
Chain = #{id => ChainID,
|
||||
type => simple},
|
||||
?assertMatch({ok, #{id := ChainID, authenticators := []}}, ?AUTH:create_chain(Chain)),
|
||||
|
||||
AuthenticatorName = <<"myauthenticator">>,
|
||||
Config = #{use_jwks => false,
|
||||
algorithm => 'hmac-based',
|
||||
|
@ -58,84 +55,74 @@ t_jwt_authenticator(_) ->
|
|||
secret_base64_encoded => false,
|
||||
verify_claims => []},
|
||||
AuthenticatorConfig = #{name => AuthenticatorName,
|
||||
type => jwt,
|
||||
mechanism => jwt,
|
||||
config => Config},
|
||||
?assertEqual({ok, AuthenticatorConfig}, ?AUTH:create_authenticator(ChainID, AuthenticatorConfig)),
|
||||
|
||||
ListenerID = <<"listener1">>,
|
||||
?AUTH:bind(ChainID, [ListenerID]),
|
||||
?assertEqual({ok, AuthenticatorConfig}, ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig)),
|
||||
|
||||
Payload = #{<<"username">> => <<"myuser">>},
|
||||
JWS = generate_jws('hmac-based', Payload, <<"abcdef">>),
|
||||
ClientInfo = #{listener_id => ListenerID,
|
||||
username => <<"myuser">>,
|
||||
ClientInfo = #{username => <<"myuser">>,
|
||||
password => JWS},
|
||||
?assertEqual(ok, ?AUTH:authenticate(ClientInfo)),
|
||||
?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo, ok)),
|
||||
|
||||
BadJWS = generate_jws('hmac-based', Payload, <<"bad_secret">>),
|
||||
ClientInfo2 = ClientInfo#{password => BadJWS},
|
||||
?assertEqual({error, user_not_found}, ?AUTH:authenticate(ClientInfo2)),
|
||||
?assertEqual({stop, {error, not_authorized}}, ?AUTH:authenticate(ClientInfo2, ok)),
|
||||
|
||||
%% secret_base64_encoded
|
||||
Config2 = Config#{secret => base64:encode(<<"abcdef">>),
|
||||
secret_base64_encoded => true},
|
||||
?assertMatch({ok, _}, ?AUTH:update_authenticator(ChainID, AuthenticatorName, Config2)),
|
||||
?assertEqual(ok, ?AUTH:authenticate(ClientInfo)),
|
||||
?assertMatch({ok, _}, ?AUTH:update_authenticator(?CHAIN, AuthenticatorName, Config2)),
|
||||
?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo, ok)),
|
||||
|
||||
Config3 = Config#{verify_claims => [{<<"username">>, <<"${mqtt-username}">>}]},
|
||||
?assertMatch({ok, _}, ?AUTH:update_authenticator(ChainID, AuthenticatorName, Config3)),
|
||||
?assertEqual(ok, ?AUTH:authenticate(ClientInfo)),
|
||||
?assertEqual({error, bad_password}, ?AUTH:authenticate(ClientInfo#{username => <<"otheruser">>})),
|
||||
?assertMatch({ok, _}, ?AUTH:update_authenticator(?CHAIN, AuthenticatorName, Config3)),
|
||||
?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo, ok)),
|
||||
?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo#{username => <<"otheruser">>}, ok)),
|
||||
|
||||
%% Expiration
|
||||
Payload3 = #{ <<"username">> => <<"myuser">>
|
||||
, <<"exp">> => erlang:system_time(second) - 60},
|
||||
JWS3 = generate_jws('hmac-based', Payload3, <<"abcdef">>),
|
||||
ClientInfo3 = ClientInfo#{password => JWS3},
|
||||
?assertEqual({error, bad_password}, ?AUTH:authenticate(ClientInfo3)),
|
||||
?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo3, ok)),
|
||||
|
||||
Payload4 = #{ <<"username">> => <<"myuser">>
|
||||
, <<"exp">> => erlang:system_time(second) + 60},
|
||||
JWS4 = generate_jws('hmac-based', Payload4, <<"abcdef">>),
|
||||
ClientInfo4 = ClientInfo#{password => JWS4},
|
||||
?assertEqual(ok, ?AUTH:authenticate(ClientInfo4)),
|
||||
?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo4, ok)),
|
||||
|
||||
%% Issued At
|
||||
Payload5 = #{ <<"username">> => <<"myuser">>
|
||||
, <<"iat">> => erlang:system_time(second) - 60},
|
||||
JWS5 = generate_jws('hmac-based', Payload5, <<"abcdef">>),
|
||||
ClientInfo5 = ClientInfo#{password => JWS5},
|
||||
?assertEqual(ok, ?AUTH:authenticate(ClientInfo5)),
|
||||
?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo5, ok)),
|
||||
|
||||
Payload6 = #{ <<"username">> => <<"myuser">>
|
||||
, <<"iat">> => erlang:system_time(second) + 60},
|
||||
JWS6 = generate_jws('hmac-based', Payload6, <<"abcdef">>),
|
||||
ClientInfo6 = ClientInfo#{password => JWS6},
|
||||
?assertEqual({error, bad_password}, ?AUTH:authenticate(ClientInfo6)),
|
||||
?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo6, ok)),
|
||||
|
||||
%% Not Before
|
||||
Payload7 = #{ <<"username">> => <<"myuser">>
|
||||
, <<"nbf">> => erlang:system_time(second) - 60},
|
||||
JWS7 = generate_jws('hmac-based', Payload7, <<"abcdef">>),
|
||||
ClientInfo7 = ClientInfo#{password => JWS7},
|
||||
?assertEqual(ok, ?AUTH:authenticate(ClientInfo7)),
|
||||
?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo7, ok)),
|
||||
|
||||
Payload8 = #{ <<"username">> => <<"myuser">>
|
||||
, <<"nbf">> => erlang:system_time(second) + 60},
|
||||
JWS8 = generate_jws('hmac-based', Payload8, <<"abcdef">>),
|
||||
ClientInfo8 = ClientInfo#{password => JWS8},
|
||||
?assertEqual({error, bad_password}, ?AUTH:authenticate(ClientInfo8)),
|
||||
?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo8, ok)),
|
||||
|
||||
?AUTH:unbind([ListenerID], ChainID),
|
||||
?assertEqual(ok, ?AUTH:delete_chain(ChainID)),
|
||||
?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, AuthenticatorName)),
|
||||
ok.
|
||||
|
||||
t_jwt_authenticator2(_) ->
|
||||
ChainID = <<"mychain">>,
|
||||
Chain = #{id => ChainID,
|
||||
type => simple},
|
||||
?assertMatch({ok, #{id := ChainID, authenticators := []}}, ?AUTH:create_chain(Chain)),
|
||||
|
||||
Dir = code:lib_dir(emqx_authn, test),
|
||||
PublicKey = list_to_binary(filename:join([Dir, "data/public_key.pem"])),
|
||||
PrivateKey = list_to_binary(filename:join([Dir, "data/private_key.pem"])),
|
||||
|
@ -145,23 +132,18 @@ t_jwt_authenticator2(_) ->
|
|||
certificate => PublicKey,
|
||||
verify_claims => []},
|
||||
AuthenticatorConfig = #{name => AuthenticatorName,
|
||||
type => jwt,
|
||||
mechanism => jwt,
|
||||
config => Config},
|
||||
?assertEqual({ok, AuthenticatorConfig}, ?AUTH:create_authenticator(ChainID, AuthenticatorConfig)),
|
||||
|
||||
ListenerID = <<"listener1">>,
|
||||
?AUTH:bind(ChainID, [ListenerID]),
|
||||
?assertEqual({ok, AuthenticatorConfig}, ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig)),
|
||||
|
||||
Payload = #{<<"username">> => <<"myuser">>},
|
||||
JWS = generate_jws('public-key', Payload, PrivateKey),
|
||||
ClientInfo = #{listener_id => ListenerID,
|
||||
username => <<"myuser">>,
|
||||
ClientInfo = #{username => <<"myuser">>,
|
||||
password => JWS},
|
||||
?assertEqual(ok, ?AUTH:authenticate(ClientInfo)),
|
||||
?assertEqual({error, user_not_found}, ?AUTH:authenticate(ClientInfo#{password => <<"badpassword">>})),
|
||||
?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo, ok)),
|
||||
?assertEqual({stop, {error, not_authorized}}, ?AUTH:authenticate(ClientInfo#{password => <<"badpassword">>}, ok)),
|
||||
|
||||
?AUTH:unbind([ListenerID], ChainID),
|
||||
?assertEqual(ok, ?AUTH:delete_chain(ChainID)),
|
||||
?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, AuthenticatorName)),
|
||||
ok.
|
||||
|
||||
generate_jws('hmac-based', Payload, Secret) ->
|
||||
|
|
|
@ -22,6 +22,8 @@
|
|||
-include_lib("common_test/include/ct.hrl").
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
|
||||
-include("emqx_authn.hrl").
|
||||
|
||||
-define(AUTH, emqx_authn).
|
||||
|
||||
all() ->
|
||||
|
@ -39,149 +41,125 @@ end_per_suite(_) ->
|
|||
set_special_configs(emqx_authn) ->
|
||||
application:set_env(emqx, plugins_etc_dir,
|
||||
emqx_ct_helpers:deps_path(emqx_authn, "test")),
|
||||
Conf = #{<<"authn">> => #{<<"chains">> => [], <<"bindings">> => []}},
|
||||
Conf = #{<<"emqx_authn">> => #{<<"authenticators">> => []}},
|
||||
ok = file:write_file(filename:join(emqx:get_env(plugins_etc_dir), 'emqx_authn.conf'), jsx:encode(Conf)),
|
||||
ok;
|
||||
set_special_configs(_App) ->
|
||||
ok.
|
||||
|
||||
t_mnesia_authenticator(_) ->
|
||||
ChainID = <<"mychain">>,
|
||||
Chain = #{id => ChainID,
|
||||
type => simple},
|
||||
?assertMatch({ok, #{id := ChainID, authenticators := []}}, ?AUTH:create_chain(Chain)),
|
||||
ct:pal("11111 ~p~n", [?AUTH:list_authenticators(<<"mqtt">>)]),
|
||||
|
||||
|
||||
AuthenticatorName = <<"myauthenticator">>,
|
||||
AuthenticatorConfig = #{name => AuthenticatorName,
|
||||
type => 'built-in-database',
|
||||
mechanism => 'password-based',
|
||||
config => #{
|
||||
server_type => 'built-in-database',
|
||||
user_id_type => username,
|
||||
password_hash_algorithm => #{
|
||||
name => sha256
|
||||
}}},
|
||||
?assertEqual({ok, AuthenticatorConfig}, ?AUTH:create_authenticator(ChainID, AuthenticatorConfig)),
|
||||
?assertEqual({ok, AuthenticatorConfig}, ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig)),
|
||||
|
||||
UserInfo = #{<<"user_id">> => <<"myuser">>,
|
||||
<<"password">> => <<"mypass">>},
|
||||
?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:add_user(ChainID, AuthenticatorName, UserInfo)),
|
||||
?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:lookup_user(ChainID, AuthenticatorName, <<"myuser">>)),
|
||||
?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:add_user(?CHAIN, AuthenticatorName, UserInfo)),
|
||||
?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:lookup_user(?CHAIN, AuthenticatorName, <<"myuser">>)),
|
||||
|
||||
ListenerID = <<"listener1">>,
|
||||
?AUTH:bind(ChainID, [ListenerID]),
|
||||
|
||||
ClientInfo = #{listener_id => ListenerID,
|
||||
username => <<"myuser">>,
|
||||
ClientInfo = #{username => <<"myuser">>,
|
||||
password => <<"mypass">>},
|
||||
?assertEqual(ok, ?AUTH:authenticate(ClientInfo)),
|
||||
?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo, ok)),
|
||||
ClientInfo2 = ClientInfo#{username => <<"baduser">>},
|
||||
?assertEqual({error, user_not_found}, ?AUTH:authenticate(ClientInfo2)),
|
||||
?assertEqual({stop, {error, not_authorized}}, ?AUTH:authenticate(ClientInfo2, ok)),
|
||||
ClientInfo3 = ClientInfo#{password => <<"badpass">>},
|
||||
?assertEqual({error, bad_password}, ?AUTH:authenticate(ClientInfo3)),
|
||||
?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo3, ok)),
|
||||
|
||||
UserInfo2 = UserInfo#{<<"password">> => <<"mypass2">>},
|
||||
?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:update_user(ChainID, AuthenticatorName, <<"myuser">>, UserInfo2)),
|
||||
?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:update_user(?CHAIN, AuthenticatorName, <<"myuser">>, UserInfo2)),
|
||||
ClientInfo4 = ClientInfo#{password => <<"mypass2">>},
|
||||
?assertEqual(ok, ?AUTH:authenticate(ClientInfo4)),
|
||||
?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo4, ok)),
|
||||
|
||||
?assertEqual(ok, ?AUTH:delete_user(ChainID, AuthenticatorName, <<"myuser">>)),
|
||||
?assertEqual({error, not_found}, ?AUTH:lookup_user(ChainID, AuthenticatorName, <<"myuser">>)),
|
||||
?assertEqual(ok, ?AUTH:delete_user(?CHAIN, AuthenticatorName, <<"myuser">>)),
|
||||
?assertEqual({error, not_found}, ?AUTH:lookup_user(?CHAIN, AuthenticatorName, <<"myuser">>)),
|
||||
|
||||
?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:add_user(ChainID, AuthenticatorName, UserInfo)),
|
||||
?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:lookup_user(ChainID, AuthenticatorName, <<"myuser">>)),
|
||||
?assertEqual(ok, ?AUTH:delete_authenticator(ChainID, AuthenticatorName)),
|
||||
?assertEqual({ok, AuthenticatorConfig}, ?AUTH:create_authenticator(ChainID, AuthenticatorConfig)),
|
||||
?assertMatch({error, not_found}, ?AUTH:lookup_user(ChainID, AuthenticatorName, <<"myuser">>)),
|
||||
?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:add_user(?CHAIN, AuthenticatorName, UserInfo)),
|
||||
?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:lookup_user(?CHAIN, AuthenticatorName, <<"myuser">>)),
|
||||
?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, AuthenticatorName)),
|
||||
|
||||
?AUTH:unbind([ListenerID], ChainID),
|
||||
?assertEqual(ok, ?AUTH:delete_chain(ChainID)),
|
||||
?assertEqual([], ets:tab2list(mnesia_basic_auth)),
|
||||
?assertEqual({ok, AuthenticatorConfig}, ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig)),
|
||||
?assertMatch({error, not_found}, ?AUTH:lookup_user(?CHAIN, AuthenticatorName, <<"myuser">>)),
|
||||
?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, AuthenticatorName)),
|
||||
ok.
|
||||
|
||||
t_import(_) ->
|
||||
ChainID = <<"mychain">>,
|
||||
Chain = #{id => ChainID,
|
||||
type => simple},
|
||||
?assertMatch({ok, #{id := ChainID, authenticators := []}}, ?AUTH:create_chain(Chain)),
|
||||
|
||||
AuthenticatorName = <<"myauthenticator">>,
|
||||
AuthenticatorConfig = #{name => AuthenticatorName,
|
||||
type => 'built-in-database',
|
||||
mechanism => 'password-based',
|
||||
config => #{
|
||||
server_type => 'built-in-database',
|
||||
user_id_type => username,
|
||||
password_hash_algorithm => #{
|
||||
name => sha256
|
||||
}}},
|
||||
?assertEqual({ok, AuthenticatorConfig}, ?AUTH:create_authenticator(ChainID, AuthenticatorConfig)),
|
||||
?assertEqual({ok, AuthenticatorConfig}, ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig)),
|
||||
|
||||
Dir = code:lib_dir(emqx_authn, test),
|
||||
?assertEqual(ok, ?AUTH:import_users(ChainID, AuthenticatorName, filename:join([Dir, "data/user-credentials.json"]))),
|
||||
?assertEqual(ok, ?AUTH:import_users(ChainID, AuthenticatorName, filename:join([Dir, "data/user-credentials.csv"]))),
|
||||
?assertMatch({ok, #{user_id := <<"myuser1">>}}, ?AUTH:lookup_user(ChainID, AuthenticatorName, <<"myuser1">>)),
|
||||
?assertMatch({ok, #{user_id := <<"myuser3">>}}, ?AUTH:lookup_user(ChainID, AuthenticatorName, <<"myuser3">>)),
|
||||
?assertEqual(ok, ?AUTH:import_users(?CHAIN, AuthenticatorName, filename:join([Dir, "data/user-credentials.json"]))),
|
||||
?assertEqual(ok, ?AUTH:import_users(?CHAIN, AuthenticatorName, filename:join([Dir, "data/user-credentials.csv"]))),
|
||||
?assertMatch({ok, #{user_id := <<"myuser1">>}}, ?AUTH:lookup_user(?CHAIN, AuthenticatorName, <<"myuser1">>)),
|
||||
?assertMatch({ok, #{user_id := <<"myuser3">>}}, ?AUTH:lookup_user(?CHAIN, AuthenticatorName, <<"myuser3">>)),
|
||||
|
||||
ListenerID = <<"listener1">>,
|
||||
?AUTH:bind(ChainID, [ListenerID]),
|
||||
|
||||
ClientInfo1 = #{listener_id => ListenerID,
|
||||
username => <<"myuser1">>,
|
||||
ClientInfo1 = #{username => <<"myuser1">>,
|
||||
password => <<"mypassword1">>},
|
||||
?assertEqual(ok, ?AUTH:authenticate(ClientInfo1)),
|
||||
?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo1, ok)),
|
||||
ClientInfo2 = ClientInfo1#{username => <<"myuser3">>,
|
||||
password => <<"mypassword3">>},
|
||||
?assertEqual(ok, ?AUTH:authenticate(ClientInfo2)),
|
||||
|
||||
?AUTH:unbind([ListenerID], ChainID),
|
||||
?assertEqual(ok, ?AUTH:delete_chain(ChainID)),
|
||||
?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo2, ok)),
|
||||
?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, AuthenticatorName)),
|
||||
ok.
|
||||
|
||||
t_multi_mnesia_authenticator(_) ->
|
||||
ChainID = <<"mychain">>,
|
||||
Chain = #{id => ChainID,
|
||||
type => simple},
|
||||
?assertMatch({ok, #{id := ChainID, authenticators := []}}, ?AUTH:create_chain(Chain)),
|
||||
|
||||
AuthenticatorName1 = <<"myauthenticator1">>,
|
||||
AuthenticatorConfig1 = #{name => AuthenticatorName1,
|
||||
type => 'built-in-database',
|
||||
mechanism => 'password-based',
|
||||
config => #{
|
||||
server_type => 'built-in-database',
|
||||
user_id_type => username,
|
||||
password_hash_algorithm => #{
|
||||
name => sha256
|
||||
}}},
|
||||
AuthenticatorName2 = <<"myauthenticator2">>,
|
||||
AuthenticatorConfig2 = #{name => AuthenticatorName2,
|
||||
type => 'built-in-database',
|
||||
mechanism => 'password-based',
|
||||
config => #{
|
||||
server_type => 'built-in-database',
|
||||
user_id_type => clientid,
|
||||
password_hash_algorithm => #{
|
||||
name => sha256
|
||||
}}},
|
||||
?assertEqual({ok, AuthenticatorConfig1}, ?AUTH:create_authenticator(ChainID, AuthenticatorConfig1)),
|
||||
?assertEqual({ok, AuthenticatorConfig2}, ?AUTH:create_authenticator(ChainID, AuthenticatorConfig2)),
|
||||
?assertEqual({ok, AuthenticatorConfig1}, ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig1)),
|
||||
?assertEqual({ok, AuthenticatorConfig2}, ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig2)),
|
||||
|
||||
?assertEqual({ok, #{user_id => <<"myuser">>}},
|
||||
?AUTH:add_user(ChainID, AuthenticatorName1,
|
||||
?AUTH:add_user(?CHAIN, AuthenticatorName1,
|
||||
#{<<"user_id">> => <<"myuser">>,
|
||||
<<"password">> => <<"mypass1">>})),
|
||||
?assertEqual({ok, #{user_id => <<"myclient">>}},
|
||||
?AUTH:add_user(ChainID, AuthenticatorName2,
|
||||
?AUTH:add_user(?CHAIN, AuthenticatorName2,
|
||||
#{<<"user_id">> => <<"myclient">>,
|
||||
<<"password">> => <<"mypass2">>})),
|
||||
|
||||
ListenerID = <<"listener1">>,
|
||||
?AUTH:bind(ChainID, [ListenerID]),
|
||||
|
||||
ClientInfo1 = #{listener_id => ListenerID,
|
||||
username => <<"myuser">>,
|
||||
ClientInfo1 = #{username => <<"myuser">>,
|
||||
clientid => <<"myclient">>,
|
||||
password => <<"mypass1">>},
|
||||
?assertEqual(ok, ?AUTH:authenticate(ClientInfo1)),
|
||||
?assertEqual(ok, ?AUTH:move_authenticator_to_the_front(ChainID, AuthenticatorName2)),
|
||||
?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo1, ok)),
|
||||
?assertEqual(ok, ?AUTH:move_authenticator_to_the_front(?CHAIN, AuthenticatorName2)),
|
||||
|
||||
?assertEqual({error, bad_password}, ?AUTH:authenticate(ClientInfo1)),
|
||||
?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo1, ok)),
|
||||
ClientInfo2 = ClientInfo1#{password => <<"mypass2">>},
|
||||
?assertEqual(ok, ?AUTH:authenticate(ClientInfo2)),
|
||||
?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo2, ok)),
|
||||
|
||||
?AUTH:unbind([ListenerID], ChainID),
|
||||
?assertEqual(ok, ?AUTH:delete_chain(ChainID)),
|
||||
?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, AuthenticatorName1)),
|
||||
?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, AuthenticatorName2)),
|
||||
ok.
|
||||
|
|
|
@ -260,25 +260,21 @@ handle_call({auth, ClientInfo0, Password},
|
|||
Channel = #channel{conninfo = ConnInfo,
|
||||
clientinfo = ClientInfo}) ->
|
||||
ClientInfo1 = enrich_clientinfo(ClientInfo0, ClientInfo),
|
||||
NConnInfo = enrich_conninfo(ClientInfo1, ConnInfo),
|
||||
ConnInfo1 = enrich_conninfo(ClientInfo1, ConnInfo),
|
||||
|
||||
Channel1 = Channel#channel{conninfo = NConnInfo,
|
||||
Channel1 = Channel#channel{conninfo = ConnInfo1,
|
||||
clientinfo = ClientInfo1},
|
||||
|
||||
#{clientid := ClientId, username := Username} = ClientInfo1,
|
||||
|
||||
case emqx_access_control:authenticate(ClientInfo1#{password => Password}) of
|
||||
{ok, AuthResult} ->
|
||||
ok ->
|
||||
emqx_logger:set_metadata_clientid(ClientId),
|
||||
is_anonymous(AuthResult) andalso
|
||||
emqx_metrics:inc('client.auth.anonymous'),
|
||||
NClientInfo = maps:merge(ClientInfo1, AuthResult),
|
||||
NChannel = Channel1#channel{clientinfo = NClientInfo},
|
||||
case emqx_cm:open_session(true, NClientInfo, NConnInfo) of
|
||||
case emqx_cm:open_session(true, ClientInfo1, ConnInfo1) of
|
||||
{ok, _Session} ->
|
||||
?LOG(debug, "Client ~s (Username: '~s') authorized successfully!",
|
||||
[ClientId, Username]),
|
||||
{reply, ok, [{event, connected}], ensure_connected(NChannel)};
|
||||
{reply, ok, [{event, connected}], ensure_connected(Channel1)};
|
||||
{error, Reason} ->
|
||||
?LOG(warning, "Client ~s (Username: '~s') open session failed for ~0p",
|
||||
[ClientId, Username, Reason]),
|
||||
|
@ -393,9 +389,6 @@ terminate(Reason, Channel) ->
|
|||
Req = #{reason => stringfy(Reason)},
|
||||
try_dispatch(on_socket_closed, wrap(Req), Channel).
|
||||
|
||||
is_anonymous(#{anonymous := true}) -> true;
|
||||
is_anonymous(_AuthResult) -> false.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Sub/UnSub
|
||||
%%--------------------------------------------------------------------
|
||||
|
|
|
@ -33,7 +33,7 @@
|
|||
%% Gateway ID
|
||||
, type := gateway_type()
|
||||
%% Autenticator
|
||||
, auth := allow_anonymous | emqx_authentication:chain_id()
|
||||
, auth := emqx_authn:chain_id()
|
||||
%% The ConnectionManager PID
|
||||
, cm := pid()
|
||||
}.
|
||||
|
@ -66,19 +66,19 @@
|
|||
-spec authenticate(context(), emqx_types:clientinfo())
|
||||
-> {ok, emqx_types:clientinfo()}
|
||||
| {error, any()}.
|
||||
authenticate(_Ctx = #{auth := allow_anonymous}, ClientInfo) ->
|
||||
{ok, ClientInfo#{anonymous => true}};
|
||||
authenticate(_Ctx = #{auth := ChainId}, ClientInfo0) ->
|
||||
ClientInfo = ClientInfo0#{
|
||||
zone => undefined,
|
||||
chain_id => ChainId
|
||||
},
|
||||
case emqx_access_control:authenticate(ClientInfo) of
|
||||
{ok, AuthResult} ->
|
||||
{ok, mountpoint(maps:merge(ClientInfo, AuthResult))};
|
||||
ok ->
|
||||
{ok, mountpoint(ClientInfo)};
|
||||
{error, Reason} ->
|
||||
{error, Reason}
|
||||
end.
|
||||
end;
|
||||
authenticate(_Ctx, ClientInfo) ->
|
||||
{ok, ClientInfo}.
|
||||
|
||||
%% @doc Register the session to the cluster.
|
||||
%%
|
||||
|
|
|
@ -86,15 +86,14 @@ init(CoapPid, EndpointName, Peername = {_Peerhost, _Port}, RegInfo = #{<<"lt">>
|
|||
ClientInfo = clientinfo(Lwm2mState),
|
||||
_ = run_hooks('client.connect', [conninfo(Lwm2mState)], undefined),
|
||||
case emqx_access_control:authenticate(ClientInfo) of
|
||||
{ok, AuthResult} ->
|
||||
ok ->
|
||||
_ = run_hooks('client.connack', [conninfo(Lwm2mState), success], undefined),
|
||||
|
||||
ClientInfo1 = maps:merge(ClientInfo, AuthResult),
|
||||
Sockport = proplists:get_value(port, lwm2m_coap_responder:options(), 5683),
|
||||
ClientInfo2 = maps:put(sockport, Sockport, ClientInfo1),
|
||||
ClientInfo1 = maps:put(sockport, Sockport, ClientInfo),
|
||||
Lwm2mState1 = Lwm2mState#lwm2m_state{started_at = time_now(),
|
||||
mountpoint = maps:get(mountpoint, ClientInfo2)},
|
||||
run_hooks('client.connected', [ClientInfo2, conninfo(Lwm2mState1)]),
|
||||
mountpoint = maps:get(mountpoint, ClientInfo1)},
|
||||
run_hooks('client.connected', [ClientInfo1, conninfo(Lwm2mState1)]),
|
||||
|
||||
erlang:send(CoapPid, post_init),
|
||||
erlang:send_after(2000, CoapPid, auto_observe),
|
||||
|
|
|
@ -63,6 +63,7 @@
|
|||
, {snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "0.13.0"}}}
|
||||
, {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.9.6"}}}
|
||||
, {emqx_http_lib, {git, "https://github.com/emqx/emqx_http_lib.git", {tag, "0.2.1"}}}
|
||||
, {esasl, {git, "https://github.com/emqx/esasl", {branch, "refactor/sasl"}}}
|
||||
]}.
|
||||
|
||||
{xref_ignores,
|
||||
|
|
Loading…
Reference in New Issue