feat(authn): support superuser

This commit is contained in:
zhouzb 2021-08-13 10:32:31 +08:00
parent 429def6b95
commit e6f9767066
19 changed files with 270 additions and 151 deletions

View File

@ -27,9 +27,14 @@
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
-spec(authenticate(emqx_types:clientinfo()) -> -spec(authenticate(emqx_types:clientinfo()) ->
ok | {ok, binary()} | {continue, map()} | {continue, binary(), map()} | {error, term()}). {ok, map()} | {ok, map(), binary()} | {continue, map()} | {continue, binary(), map()} | {error, term()}).
authenticate(Credential) -> authenticate(Credential) ->
run_hooks('client.authenticate', [Credential], ok). case run_hooks('client.authenticate', [Credential], {ok, #{superuser => false}}) of
ok ->
{ok, #{superuser => false}};
Other ->
Other
end.
%% @doc Check Authorization %% @doc Check Authorization
-spec authorize(emqx_types:clientinfo(), emqx_types:pubsub(), emqx_types:topic()) -spec authorize(emqx_types:clientinfo(), emqx_types:pubsub(), emqx_types:topic())

View File

@ -1299,14 +1299,17 @@ authenticate(?AUTH_PACKET(_, #{'Authentication-Method' := AuthMethod} = Properti
{error, ?RC_BAD_AUTHENTICATION_METHOD} {error, ?RC_BAD_AUTHENTICATION_METHOD}
end. end.
do_authenticate(#{auth_method := AuthMethod} = Credential, Channel) -> do_authenticate(#{auth_method := AuthMethod} = Credential, #channel{clientinfo = ClientInfo} = Channel) ->
Properties = #{'Authentication-Method' => AuthMethod}, Properties = #{'Authentication-Method' => AuthMethod},
case emqx_access_control:authenticate(Credential) of case emqx_access_control:authenticate(Credential) of
ok -> {ok, #{superuser := Superuser}} ->
{ok, Properties, Channel#channel{auth_cache = #{}}}; {ok, Properties,
{ok, AuthData} -> Channel#channel{clientinfo = ClientInfo#{is_superuser => Superuser},
auth_cache = #{}}};
{ok, #{superuser := Superuser}, AuthData} ->
{ok, Properties#{'Authentication-Data' => AuthData}, {ok, Properties#{'Authentication-Data' => AuthData},
Channel#channel{auth_cache = #{}}}; Channel#channel{clientinfo = ClientInfo#{is_superuser => Superuser},
auth_cache = #{}}};
{continue, AuthCache} -> {continue, AuthCache} ->
{continue, Properties, Channel#channel{auth_cache = AuthCache}}; {continue, Properties, Channel#channel{auth_cache = AuthCache}};
{continue, AuthData, AuthCache} -> {continue, AuthData, AuthCache} ->
@ -1316,10 +1319,10 @@ do_authenticate(#{auth_method := AuthMethod} = Credential, Channel) ->
{error, emqx_reason_codes:connack_error(Reason)} {error, emqx_reason_codes:connack_error(Reason)}
end; end;
do_authenticate(Credential, Channel) -> do_authenticate(Credential, #channel{clientinfo = ClientInfo} = Channel) ->
case emqx_access_control:authenticate(Credential) of case emqx_access_control:authenticate(Credential) of
ok -> {ok, #{superuser := Superuser}} ->
{ok, #{}, Channel}; {ok, #{}, Channel#channel{clientinfo = ClientInfo#{is_superuser => Superuser}}};
{error, Reason} -> {error, Reason} ->
{error, emqx_reason_codes:connack_error(Reason)} {error, emqx_reason_codes:connack_error(Reason)}
end. end.

View File

@ -1,3 +1,3 @@
user_id,password_hash,salt user_id,password_hash,salt,superuser
myuser3,b6c743545a7817ae8c8f624371d5f5f0373234bb0ff36b8ffbf19bce0e06ab75,de1024f462fb83910fd13151bd4bd235 myuser3,b6c743545a7817ae8c8f624371d5f5f0373234bb0ff36b8ffbf19bce0e06ab75,de1024f462fb83910fd13151bd4bd235,true
myuser4,ee68c985a69208b6eda8c6c9b4c7c2d2b15ee2352cdd64a903171710a99182e8,ad773b5be9dd0613fe6c2f4d8c403139 myuser4,ee68c985a69208b6eda8c6c9b4c7c2d2b15ee2352cdd64a903171710a99182e8,ad773b5be9dd0613fe6c2f4d8c403139,false

1 user_id password_hash salt superuser
2 myuser3 b6c743545a7817ae8c8f624371d5f5f0373234bb0ff36b8ffbf19bce0e06ab75 de1024f462fb83910fd13151bd4bd235 true
3 myuser4 ee68c985a69208b6eda8c6c9b4c7c2d2b15ee2352cdd64a903171710a99182e8 ad773b5be9dd0613fe6c2f4d8c403139 false

View File

@ -2,11 +2,13 @@
{ {
"user_id":"myuser1", "user_id":"myuser1",
"password_hash":"c5e46903df45e5dc096dc74657610dbee8deaacae656df88a1788f1847390242", "password_hash":"c5e46903df45e5dc096dc74657610dbee8deaacae656df88a1788f1847390242",
"salt": "e378187547bf2d6f0545a3f441aa4d8a" "salt": "e378187547bf2d6f0545a3f441aa4d8a",
"superuser": true
}, },
{ {
"user_id":"myuser2", "user_id":"myuser2",
"password_hash":"f4d17f300b11e522fd33f497c11b126ef1ea5149c74d2220f9a16dc876d4567b", "password_hash":"f4d17f300b11e522fd33f497c11b126ef1ea5149c74d2220f9a16dc876d4567b",
"salt": "6d3f9bd5b54d94b98adbcfe10b6d181f" "salt": "6d3f9bd5b54d94b98adbcfe10b6d181f",
"superuser": false
} }
] ]

View File

@ -235,8 +235,9 @@ do_authenticate([{_, _, #authenticator{provider = Provider, state = State}} | Mo
ignore -> ignore ->
do_authenticate(More, Credential); do_authenticate(More, Credential);
Result -> Result ->
%% ok %% {ok, Extra}
%% {ok, AuthData} %% {ok, Extra, AuthData}
%% {ok, MetaData}
%% {continue, AuthCache} %% {continue, AuthCache}
%% {continue, AuthData, AuthCache} %% {continue, AuthData, AuthCache}
%% {error, Reason} %% {error, Reason}

View File

@ -528,6 +528,10 @@ users_api() ->
}, },
password => #{ password => #{
type => string type => string
},
superuser => #{
type => boolean,
default => false
} }
} }
} }
@ -541,10 +545,12 @@ users_api() ->
'application/json' => #{ 'application/json' => #{
schema => #{ schema => #{
type => object, type => object,
required => [user_id],
properties => #{ properties => #{
user_id => #{ user_id => #{
type => string type => string
},
superuser => #{
type => boolean
} }
} }
} }
@ -576,10 +582,12 @@ users_api() ->
type => array, type => array,
items => #{ items => #{
type => object, type => object,
required => [user_id],
properties => #{ properties => #{
user_id => #{ user_id => #{
type => string type => string
},
superuser => #{
type => boolean
} }
} }
} }
@ -620,10 +628,12 @@ users2_api() ->
'application/json' => #{ 'application/json' => #{
schema => #{ schema => #{
type => object, type => object,
required => [password],
properties => #{ properties => #{
password => #{ password => #{
type => string type => string
},
superuser => #{
type => boolean
} }
} }
} }
@ -642,6 +652,9 @@ users2_api() ->
properties => #{ properties => #{
user_id => #{ user_id => #{
type => string type => string
},
superuser => #{
type => boolean
} }
} }
} }
@ -685,6 +698,9 @@ users2_api() ->
properties => #{ properties => #{
user_id => #{ user_id => #{
type => string type => string
},
superuser => #{
type => boolean
} }
} }
} }
@ -1359,9 +1375,11 @@ users(post, Request) ->
{ok, Body, _} = cowboy_req:read_body(Request), {ok, Body, _} = cowboy_req:read_body(Request),
case emqx_json:decode(Body, [return_maps]) of case emqx_json:decode(Body, [return_maps]) of
#{ <<"user_id">> := UserID #{ <<"user_id">> := UserID
, <<"password">> := Password} -> , <<"password">> := Password} = UserInfo ->
Superuser = maps:get(<<"superuser">>, UserInfo, false),
case emqx_authn:add_user(?CHAIN, AuthenticatorID, #{ user_id => UserID case emqx_authn:add_user(?CHAIN, AuthenticatorID, #{ user_id => UserID
, password => Password}) of , password => Password
, superuser => Superuser}) of
{ok, User} -> {ok, User} ->
{201, User}; {201, User};
{error, Reason} -> {error, Reason} ->
@ -1385,16 +1403,18 @@ users2(patch, Request) ->
AuthenticatorID = cowboy_req:binding(id, Request), AuthenticatorID = cowboy_req:binding(id, Request),
UserID = cowboy_req:binding(user_id, Request), UserID = cowboy_req:binding(user_id, Request),
{ok, Body, _} = cowboy_req:read_body(Request), {ok, Body, _} = cowboy_req:read_body(Request),
case emqx_json:decode(Body, [return_maps]) of UserInfo = emqx_json:decode(Body, [return_maps]),
#{<<"password">> := Password} -> NUserInfo = maps:with([<<"password">>, <<"superuser">>], UserInfo),
case emqx_authn:update_user(?CHAIN, AuthenticatorID, UserID, #{password => Password}) of case NUserInfo =:= #{} of
true ->
serialize_error({missing_parameter, password});
false ->
case emqx_authn:update_user(?CHAIN, AuthenticatorID, UserID, UserInfo) of
{ok, User} -> {ok, User} ->
{200, User}; {200, User};
{error, Reason} -> {error, Reason} ->
serialize_error(Reason) serialize_error(Reason)
end; end
_ ->
serialize_error({missing_parameter, password})
end; end;
users2(get, Request) -> users2(get, Request) ->
AuthenticatorID = cowboy_req:binding(id, Request), AuthenticatorID = cowboy_req:binding(id, Request),

View File

@ -17,7 +17,6 @@
-module(emqx_enhanced_authn_scram_mnesia). -module(emqx_enhanced_authn_scram_mnesia).
-include("emqx_authn.hrl"). -include("emqx_authn.hrl").
-include_lib("esasl/include/esasl_scram.hrl").
-include_lib("typerefl/include/types.hrl"). -include_lib("typerefl/include/types.hrl").
-behaviour(hocon_schema). -behaviour(hocon_schema).
@ -48,6 +47,14 @@
-rlog_shard({?AUTH_SHARD, ?TAB}). -rlog_shard({?AUTH_SHARD, ?TAB}).
-record(user_info,
{ user_id
, stored_key
, server_key
, salt
, superuser
}).
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
%% Mnesia bootstrap %% Mnesia bootstrap
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
@ -57,8 +64,8 @@
mnesia(boot) -> mnesia(boot) ->
ok = ekka_mnesia:create_table(?TAB, [ ok = ekka_mnesia:create_table(?TAB, [
{disc_copies, [node()]}, {disc_copies, [node()]},
{record_name, scram_user_credentail}, {record_name, user_info},
{attributes, record_info(fields, scram_user_credentail)}, {attributes, record_info(fields, user_info)},
{storage_properties, [{ets, [{read_concurrency, true}]}]}]); {storage_properties, [{ets, [{read_concurrency, true}]}]}]);
mnesia(copy) -> mnesia(copy) ->
@ -126,20 +133,21 @@ authenticate(_Credential, _State) ->
destroy(#{user_group := UserGroup}) -> destroy(#{user_group := UserGroup}) ->
trans( trans(
fun() -> fun() ->
MatchSpec = [{{scram_user_credentail, {UserGroup, '_'}, '_', '_', '_'}, [], ['$_']}], MatchSpec = [{{user_info, {UserGroup, '_'}, '_', '_', '_', '_'}, [], ['$_']}],
ok = lists:foreach(fun(UserCredential) -> ok = lists:foreach(fun(UserInfo) ->
mnesia:delete_object(?TAB, UserCredential, write) mnesia:delete_object(?TAB, UserInfo, write)
end, mnesia:select(?TAB, MatchSpec, write)) end, mnesia:select(?TAB, MatchSpec, write))
end). end).
add_user(#{user_id := UserID, add_user(#{user_id := UserID,
password := Password}, #{user_group := UserGroup} = State) -> password := Password} = UserInfo, #{user_group := UserGroup} = State) ->
trans( trans(
fun() -> fun() ->
case mnesia:read(?TAB, {UserGroup, UserID}, write) of case mnesia:read(?TAB, {UserGroup, UserID}, write) of
[] -> [] ->
add_user(UserID, Password, State), Superuser = maps:get(superuser, UserInfo, false),
{ok, #{user_id => UserID}}; add_user(UserID, Password, Superuser, State),
{ok, #{user_id => UserID, superuser => Superuser}};
[_] -> [_] ->
{error, already_exist} {error, already_exist}
end end
@ -156,31 +164,41 @@ delete_user(UserID, #{user_group := UserGroup}) ->
end end
end). end).
update_user(UserID, #{password := Password}, update_user(UserID, User,
#{user_group := UserGroup} = State) -> #{user_group := UserGroup} = State) ->
trans( trans(
fun() -> fun() ->
case mnesia:read(?TAB, {UserGroup, UserID}, write) of case mnesia:read(?TAB, {UserGroup, UserID}, write) of
[] -> [] ->
{error, not_found}; {error, not_found};
[_] -> [#user_info{superuser = Superuser} = UserInfo] ->
add_user(UserID, Password, State), UserInfo1 = UserInfo#user_info{superuser = maps:get(superuser, User, Superuser)},
{ok, #{user_id => UserID}} UserInfo2 = case maps:get(password, User, undefined) of
undefined ->
UserInfo1;
Password ->
{StoredKey, ServerKey, Salt} = esasl_scram:generate_user_credential(Password, State),
UserInfo1#user_info{stored_key = StoredKey,
server_key = ServerKey,
salt = Salt}
end,
mnesia:write(?TAB, UserInfo2, write),
{ok, serialize_user_info(UserInfo2)}
end end
end). end).
lookup_user(UserID, #{user_group := UserGroup}) -> lookup_user(UserID, #{user_group := UserGroup}) ->
case mnesia:dirty_read(?TAB, {UserGroup, UserID}) of case mnesia:dirty_read(?TAB, {UserGroup, UserID}) of
[#scram_user_credentail{user_id = {_, UserID}}] -> [UserInfo] ->
{ok, #{user_id => UserID}}; {ok, serialize_user_info(UserInfo)};
[] -> [] ->
{error, not_found} {error, not_found}
end. end.
%% TODO: Support Pagination %% TODO: Support Pagination
list_users(#{user_group := UserGroup}) -> list_users(#{user_group := UserGroup}) ->
Users = [#{user_id => UserID} || Users = [serialize_user_info(UserInfo) ||
#scram_user_credentail{user_id = {UserGroup0, UserID}} <- ets:tab2list(?TAB), UserGroup0 =:= UserGroup], #user_info{user_id = {UserGroup0, _}} = UserInfo <- ets:tab2list(?TAB), UserGroup0 =:= UserGroup],
{ok, Users}. {ok, Users}.
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
@ -195,13 +213,13 @@ ensure_auth_method(_, _) ->
false. false.
check_client_first_message(Bin, _Cache, #{iteration_count := IterationCount} = State) -> check_client_first_message(Bin, _Cache, #{iteration_count := IterationCount} = State) ->
LookupFun = fun(Username) -> RetrieveFun = fun(Username) ->
lookup_user2(Username, State) retrieve(Username, State)
end, end,
case esasl_scram:check_client_first_message( case esasl_scram:check_client_first_message(
Bin, Bin,
#{iteration_count => IterationCount, #{iteration_count => IterationCount,
lookup => LookupFun} retrieve => RetrieveFun}
) of ) of
{cotinue, ServerFirstMessage, Cache} -> {cotinue, ServerFirstMessage, Cache} ->
{cotinue, ServerFirstMessage, Cache}; {cotinue, ServerFirstMessage, Cache};
@ -209,25 +227,36 @@ check_client_first_message(Bin, _Cache, #{iteration_count := IterationCount} = S
{error, not_authorized} {error, not_authorized}
end. end.
check_client_final_message(Bin, Cache, #{algorithm := Alg}) -> check_client_final_message(Bin, #{superuser := Superuser} = Cache, #{algorithm := Alg}) ->
case esasl_scram:check_client_final_message( case esasl_scram:check_client_final_message(
Bin, Bin,
Cache#{algorithm => Alg} Cache#{algorithm => Alg}
) of ) of
{ok, ServerFinalMessage} -> {ok, ServerFinalMessage} ->
{ok, ServerFinalMessage}; {ok, #{superuser => Superuser}, ServerFinalMessage};
{error, _Reason} -> {error, _Reason} ->
{error, not_authorized} {error, not_authorized}
end. end.
add_user(UserID, Password, State) -> add_user(UserID, Password, Superuser, State) ->
UserCredential = esasl_scram:generate_user_credential(UserID, Password, State), {StoredKey, ServerKey, Salt} = esasl_scram:generate_user_credential(Password, State),
mnesia:write(?TAB, UserCredential, write). UserInfo = #user_info{user_id = UserID,
stored_key = StoredKey,
server_key = ServerKey,
salt = Salt,
superuser = Superuser},
mnesia:write(?TAB, UserInfo, write).
lookup_user2(UserID, #{user_group := UserGroup}) -> retrieve(UserID, #{user_group := UserGroup}) ->
case mnesia:dirty_read(?TAB, {UserGroup, UserID}) of case mnesia:dirty_read(?TAB, {UserGroup, UserID}) of
[#scram_user_credentail{} = UserCredential] -> [#user_info{stored_key = StoredKey,
{ok, UserCredential}; server_key = ServerKey,
salt = Salt,
superuser = Superuser}] ->
{ok, #{stored_key => StoredKey,
server_key => ServerKey,
salt => Salt,
superuser => Superuser}};
[] -> [] ->
{error, not_found} {error, not_found}
end. end.
@ -241,3 +270,6 @@ trans(Fun, Args) ->
{atomic, Res} -> Res; {atomic, Res} -> Res;
{aborted, Reason} -> {error, Reason} {aborted, Reason} -> {error, Reason}
end. end.
serialize_user_info(#user_info{user_id = {_, UserID}, superuser = Superuser}) ->
#{user_id => UserID, superuser => Superuser}.

View File

@ -154,15 +154,16 @@ authenticate(Credential, #{'_unique' := Unique,
try try
Request = generate_request(Credential, State), Request = generate_request(Credential, State),
case emqx_resource:query(Unique, {Method, Request, RequestTimeout}) of case emqx_resource:query(Unique, {Method, Request, RequestTimeout}) of
{ok, 204, _Headers} -> ok; {ok, 204, _Headers} -> {ok, #{superuser => false}};
{ok, 200, Headers, Body} -> {ok, 200, Headers, Body} ->
ContentType = proplists:get_value(<<"content-type">>, Headers, <<"application/json">>), ContentType = proplists:get_value(<<"content-type">>, Headers, <<"application/json">>),
case safely_parse_body(ContentType, Body) of case safely_parse_body(ContentType, Body) of
{ok, _NBody} -> {ok, NBody} ->
%% TODO: Return by user property %% TODO: Return by user property
ok; {ok, #{superuser => maps:get(<<"superuser">>, NBody, false),
user_property => NBody}};
{error, _Reason} -> {error, _Reason} ->
ok {ok, #{superuser => false}}
end; end;
{error, _Reason} -> {error, _Reason} ->
ignore ignore
@ -291,8 +292,8 @@ safely_parse_body(ContentType, Body) ->
end. end.
parse_body(<<"application/json">>, Body) -> parse_body(<<"application/json">>, Body) ->
{ok, emqx_json:decode(Body)}; {ok, emqx_json:decode(Body, [return_maps])};
parse_body(<<"application/x-www-form-urlencoded">>, Body) -> parse_body(<<"application/x-www-form-urlencoded">>, Body) ->
{ok, cow_qs:parse_qs(Body)}; {ok, maps:from_list(cow_qs:parse_qs(Body))};
parse_body(ContentType, _) -> parse_body(ContentType, _) ->
{error, {unsupported_content_type, ContentType}}. {error, {unsupported_content_type, ContentType}}.

View File

@ -169,7 +169,7 @@ authenticate(Credential = #{password := JWT}, #{jwk := JWK,
end, end,
VerifyClaims = replace_placeholder(VerifyClaims0, Credential), VerifyClaims = replace_placeholder(VerifyClaims0, Credential),
case verify(JWT, JWKs, VerifyClaims) of case verify(JWT, JWKs, VerifyClaims) of
ok -> ok; {ok, Extra} -> {ok, Extra};
{error, invalid_signature} -> ignore; {error, invalid_signature} -> ignore;
{error, {claims, _}} -> {error, bad_username_or_password} {error, {claims, _}} -> {error, bad_username_or_password}
end. end.
@ -239,7 +239,12 @@ verify(JWS, [JWK | More], VerifyClaims) ->
try jose_jws:verify(JWK, JWS) of try jose_jws:verify(JWK, JWS) of
{true, Payload, _JWS} -> {true, Payload, _JWS} ->
Claims = emqx_json:decode(Payload, [return_maps]), Claims = emqx_json:decode(Payload, [return_maps]),
verify_claims(Claims, VerifyClaims); case verify_claims(Claims, VerifyClaims) of
ok ->
{ok, #{superuser => maps:get(<<"superuser">>, Claims, false)}};
{error, Reason} ->
{error, Reason}
end;
{false, _, _} -> {false, _, _} ->
verify(JWS, More, VerifyClaims) verify(JWS, More, VerifyClaims)
catch catch

View File

@ -46,6 +46,7 @@
{ user_id :: {user_group(), user_id()} { user_id :: {user_group(), user_id()}
, password_hash :: binary() , password_hash :: binary()
, salt :: binary() , salt :: binary()
, superuser :: boolean()
}). }).
-reflect_type([ user_id_type/0 ]). -reflect_type([ user_id_type/0 ]).
@ -147,13 +148,13 @@ authenticate(#{password := Password} = Credential,
case mnesia:dirty_read(?TAB, {UserGroup, UserID}) of case mnesia:dirty_read(?TAB, {UserGroup, UserID}) of
[] -> [] ->
ignore; ignore;
[#user_info{password_hash = PasswordHash, salt = Salt0}] -> [#user_info{password_hash = PasswordHash, salt = Salt0, superuser = Superuser}] ->
Salt = case Algorithm of Salt = case Algorithm of
bcrypt -> PasswordHash; bcrypt -> PasswordHash;
_ -> Salt0 _ -> Salt0
end, end,
case PasswordHash =:= hash(Algorithm, Password, Salt) of case PasswordHash =:= hash(Algorithm, Password, Salt) of
true -> ok; true -> {ok, #{superuser => Superuser}};
false -> {error, bad_username_or_password} false -> {error, bad_username_or_password}
end end
end. end.
@ -161,7 +162,7 @@ authenticate(#{password := Password} = Credential,
destroy(#{user_group := UserGroup}) -> destroy(#{user_group := UserGroup}) ->
trans( trans(
fun() -> fun() ->
MatchSpec = [{{user_info, {UserGroup, '_'}, '_', '_'}, [], ['$_']}], MatchSpec = [{{user_info, {UserGroup, '_'}, '_', '_', '_'}, [], ['$_']}],
ok = lists:foreach(fun delete_user2/1, mnesia:select(?TAB, MatchSpec, write)) ok = lists:foreach(fun delete_user2/1, mnesia:select(?TAB, MatchSpec, write))
end). end).
@ -179,14 +180,16 @@ import_users(Filename0, State) ->
end. end.
add_user(#{user_id := UserID, add_user(#{user_id := UserID,
password := Password}, password := Password} = UserInfo,
#{user_group := UserGroup} = State) -> #{user_group := UserGroup} = State) ->
trans( trans(
fun() -> fun() ->
case mnesia:read(?TAB, {UserGroup, UserID}, write) of case mnesia:read(?TAB, {UserGroup, UserID}, write) of
[] -> [] ->
add(UserID, Password, State), {PasswordHash, Salt} = hash(Password, State),
{ok, #{user_id => UserID}}; Superuser = maps:get(superuser, UserInfo, false),
insert_user(UserGroup, UserID, PasswordHash, Salt, Superuser),
{ok, #{user_id => UserID, superuser => Superuser}};
[_] -> [_] ->
{error, already_exist} {error, already_exist}
end end
@ -203,29 +206,38 @@ delete_user(UserID, #{user_group := UserGroup}) ->
end end
end). end).
update_user(UserID, #{password := Password}, update_user(UserID, UserInfo,
#{user_group := UserGroup} = State) -> #{user_group := UserGroup} = State) ->
trans( trans(
fun() -> fun() ->
case mnesia:read(?TAB, {UserGroup, UserID}, write) of case mnesia:read(?TAB, {UserGroup, UserID}, write) of
[] -> [] ->
{error, not_found}; {error, not_found};
[_] -> [#user_info{ password_hash = PasswordHash
add(UserID, Password, State), , salt = Salt
{ok, #{user_id => UserID}} , superuser = Superuser}] ->
NSuperuser = maps:get(superuser, UserInfo, Superuser),
{NPasswordHash, NSalt} = case maps:get(password, UserInfo, undefined) of
undefined ->
{PasswordHash, Salt};
Password ->
hash(Password, State)
end,
insert_user(UserGroup, UserID, NPasswordHash, NSalt, NSuperuser),
{ok, #{user_id => UserID, superuser => NSuperuser}}
end end
end). end).
lookup_user(UserID, #{user_group := UserGroup}) -> lookup_user(UserID, #{user_group := UserGroup}) ->
case mnesia:dirty_read(?TAB, {UserGroup, UserID}) of case mnesia:dirty_read(?TAB, {UserGroup, UserID}) of
[#user_info{user_id = {_, UserID}}] -> [UserInfo] ->
{ok, #{user_id => UserID}}; {ok, serialize_user_info(UserInfo)};
[] -> [] ->
{error, not_found} {error, not_found}
end. end.
list_users(#{user_group := UserGroup}) -> list_users(#{user_group := UserGroup}) ->
Users = [#{user_id => UserID} || #user_info{user_id = {UserGroup0, UserID}} <- ets:tab2list(?TAB), UserGroup0 =:= UserGroup], Users = [serialize_user_info(UserInfo) || #user_info{user_id = {UserGroup0, _}} = UserInfo <- ets:tab2list(?TAB), UserGroup0 =:= UserGroup],
{ok, Users}. {ok, Users}.
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
@ -268,7 +280,8 @@ import(UserGroup, [#{<<"user_id">> := UserID,
<<"password_hash">> := PasswordHash} = UserInfo | More]) <<"password_hash">> := PasswordHash} = UserInfo | More])
when is_binary(UserID) andalso is_binary(PasswordHash) -> when is_binary(UserID) andalso is_binary(PasswordHash) ->
Salt = maps:get(<<"salt">>, UserInfo, <<>>), Salt = maps:get(<<"salt">>, UserInfo, <<>>),
insert_user(UserGroup, UserID, PasswordHash, Salt), Superuser = maps:get(<<"superuser">>, UserInfo, false),
insert_user(UserGroup, UserID, PasswordHash, Salt, Superuser),
import(UserGroup, More); import(UserGroup, More);
import(_UserGroup, [_ | _More]) -> import(_UserGroup, [_ | _More]) ->
{error, bad_format}. {error, bad_format}.
@ -282,7 +295,8 @@ import(UserGroup, File, Seq) ->
{ok, #{user_id := UserID, {ok, #{user_id := UserID,
password_hash := PasswordHash} = UserInfo} -> password_hash := PasswordHash} = UserInfo} ->
Salt = maps:get(salt, UserInfo, <<>>), Salt = maps:get(salt, UserInfo, <<>>),
insert_user(UserGroup, UserID, PasswordHash, Salt), Superuser = maps:get(superuser, UserInfo, false),
insert_user(UserGroup, UserID, PasswordHash, Salt, Superuser),
import(UserGroup, File, Seq); import(UserGroup, File, Seq);
{error, Reason} -> {error, Reason} ->
{error, Reason} {error, Reason}
@ -307,8 +321,6 @@ get_csv_header(File) ->
get_user_info_by_seq(Fields, Seq) -> get_user_info_by_seq(Fields, Seq) ->
get_user_info_by_seq(Fields, Seq, #{}). get_user_info_by_seq(Fields, Seq, #{}).
get_user_info_by_seq([], [], #{user_id := _, password_hash := _, salt := _} = Acc) ->
{ok, Acc};
get_user_info_by_seq([], [], #{user_id := _, password_hash := _} = Acc) -> get_user_info_by_seq([], [], #{user_id := _, password_hash := _} = Acc) ->
{ok, Acc}; {ok, Acc};
get_user_info_by_seq(_, [], _) -> get_user_info_by_seq(_, [], _) ->
@ -319,19 +331,13 @@ get_user_info_by_seq([PasswordHash | More1], [<<"password_hash">> | More2], Acc)
get_user_info_by_seq(More1, More2, Acc#{password_hash => PasswordHash}); get_user_info_by_seq(More1, More2, Acc#{password_hash => PasswordHash});
get_user_info_by_seq([Salt | More1], [<<"salt">> | More2], Acc) -> get_user_info_by_seq([Salt | More1], [<<"salt">> | More2], Acc) ->
get_user_info_by_seq(More1, More2, Acc#{salt => Salt}); get_user_info_by_seq(More1, More2, Acc#{salt => Salt});
get_user_info_by_seq([<<"true">> | More1], [<<"superuser">> | More2], Acc) ->
get_user_info_by_seq(More1, More2, Acc#{superuser => true});
get_user_info_by_seq([<<"false">> | More1], [<<"superuser">> | More2], Acc) ->
get_user_info_by_seq(More1, More2, Acc#{superuser => false});
get_user_info_by_seq(_, _, _) -> get_user_info_by_seq(_, _, _) ->
{error, bad_format}. {error, bad_format}.
-compile({inline, [add/3]}).
add(UserID, Password, #{user_group := UserGroup,
password_hash_algorithm := Algorithm} = State) ->
Salt = gen_salt(State),
PasswordHash = hash(Algorithm, Password, Salt),
case Algorithm of
bcrypt -> insert_user(UserGroup, UserID, PasswordHash);
_ -> insert_user(UserGroup, UserID, PasswordHash, Salt)
end.
gen_salt(#{password_hash_algorithm := plain}) -> gen_salt(#{password_hash_algorithm := plain}) ->
<<>>; <<>>;
gen_salt(#{password_hash_algorithm := bcrypt, gen_salt(#{password_hash_algorithm := bcrypt,
@ -347,13 +353,16 @@ hash(bcrypt, Password, Salt) ->
hash(Algorithm, Password, Salt) -> hash(Algorithm, Password, Salt) ->
emqx_passwd:hash(Algorithm, <<Salt/binary, Password/binary>>). emqx_passwd:hash(Algorithm, <<Salt/binary, Password/binary>>).
insert_user(UserGroup, UserID, PasswordHash) -> hash(Password, #{password_hash_algorithm := Algorithm} = State) ->
insert_user(UserGroup, UserID, PasswordHash, <<>>). Salt = gen_salt(State),
PasswordHash = hash(Algorithm, Password, Salt),
{PasswordHash, Salt}.
insert_user(UserGroup, UserID, PasswordHash, Salt) -> insert_user(UserGroup, UserID, PasswordHash, Salt, Superuser) ->
UserInfo = #user_info{user_id = {UserGroup, UserID}, UserInfo = #user_info{user_id = {UserGroup, UserID},
password_hash = PasswordHash, password_hash = PasswordHash,
salt = Salt}, salt = Salt,
superuser = Superuser},
mnesia:write(?TAB, UserInfo, write). mnesia:write(?TAB, UserInfo, write).
delete_user2(UserInfo) -> delete_user2(UserInfo) ->
@ -376,8 +385,10 @@ trans(Fun, Args) ->
{aborted, Reason} -> {error, Reason} {aborted, Reason} -> {error, Reason}
end. end.
to_binary(B) when is_binary(B) -> to_binary(B) when is_binary(B) ->
B; B;
to_binary(L) when is_list(L) -> to_binary(L) when is_list(L) ->
iolist_to_binary(L). iolist_to_binary(L).
serialize_user_info(#user_info{user_id = {_, UserID}, superuser = Superuser}) ->
#{user_id => UserID, superuser => Superuser}.

View File

@ -140,7 +140,8 @@ authenticate(#{password := Password} = Credential,
ignore; ignore;
Doc -> Doc ->
case check_password(Password, Doc, State) of case check_password(Password, Doc, State) of
ok -> ok; ok ->
{ok, #{superuser => superuser(Doc, State)}};
{error, {cannot_find_password_hash_field, PasswordHashField}} -> {error, {cannot_find_password_hash_field, PasswordHashField}} ->
?LOG(error, "['~s'] Can't find password hash field: ~s", [Unique, PasswordHashField]), ?LOG(error, "['~s'] Can't find password hash field: ~s", [Unique, PasswordHashField]),
{error, bad_username_or_password}; {error, bad_username_or_password};
@ -221,6 +222,11 @@ check_password(Password,
end end
end. end.
superuser(Doc, #{superuser_field := SuperuserField}) ->
maps:get(SuperuserField, Doc, false);
superuser(_, _) ->
false.
hash(Algorithm, Password, Salt, prefix) -> hash(Algorithm, Password, Salt, prefix) ->
emqx_passwd:hash(Algorithm, <<Salt/binary, Password/binary>>); emqx_passwd:hash(Algorithm, <<Salt/binary, Password/binary>>);
hash(Algorithm, Password, Salt, suffix) -> hash(Algorithm, Password, Salt, suffix) ->

View File

@ -112,15 +112,19 @@ authenticate(#{password := Password} = Credential,
case emqx_resource:query(Unique, {sql, Query, Params, Timeout}) of case emqx_resource:query(Unique, {sql, Query, Params, Timeout}) of
{ok, _Columns, []} -> ignore; {ok, _Columns, []} -> ignore;
{ok, Columns, Rows} -> {ok, Columns, Rows} ->
%% TODO: Support superuser
Selected = maps:from_list(lists:zip(Columns, Rows)), Selected = maps:from_list(lists:zip(Columns, Rows)),
check_password(Password, Selected, State); case check_password(Password, Selected, State) of
ok ->
{ok, #{superuser => maps:get(<<"superuser">>, Selected, false)}};
{error, Reason} ->
{error, Reason}
end;
{error, _Reason} -> {error, _Reason} ->
ignore ignore
end end
catch catch
error:Reason -> error:Error ->
?LOG(warning, "The following error occurred in '~s' during authentication: ~p", [Unique, Reason]), ?LOG(warning, "The following error occurred in '~s' during authentication: ~p", [Unique, Error]),
ignore ignore
end. end.
@ -135,17 +139,17 @@ destroy(#{'_unique' := Unique}) ->
check_password(undefined, _Selected, _State) -> check_password(undefined, _Selected, _State) ->
{error, bad_username_or_password}; {error, bad_username_or_password};
check_password(Password, check_password(Password,
#{password_hash := Hash}, #{<<"password_hash">> := Hash},
#{password_hash_algorithm := bcrypt}) -> #{password_hash_algorithm := bcrypt}) ->
case {ok, Hash} =:= bcrypt:hashpw(Password, Hash) of case {ok, Hash} =:= bcrypt:hashpw(Password, Hash) of
true -> ok; true -> ok;
false -> {error, bad_username_or_password} false -> {error, bad_username_or_password}
end; end;
check_password(Password, check_password(Password,
#{password_hash := Hash} = Selected, #{<<"password_hash">> := Hash} = Selected,
#{password_hash_algorithm := Algorithm, #{password_hash_algorithm := Algorithm,
salt_position := SaltPosition}) -> salt_position := SaltPosition}) ->
Salt = maps:get(salt, Selected, <<>>), Salt = maps:get(<<"salt">>, Selected, <<>>),
case Hash =:= emqx_authn_utils:hash(Algorithm, Password, Salt, SaltPosition) of case Hash =:= emqx_authn_utils:hash(Algorithm, Password, Salt, SaltPosition) of
true -> ok; true -> ok;
false -> {error, bad_username_or_password} false -> {error, bad_username_or_password}

View File

@ -18,6 +18,7 @@
-include("emqx_authn.hrl"). -include("emqx_authn.hrl").
-include_lib("emqx/include/logger.hrl"). -include_lib("emqx/include/logger.hrl").
-include_lib("epgsql/include/epgsql.hrl").
-include_lib("typerefl/include/types.hrl"). -include_lib("typerefl/include/types.hrl").
-behaviour(hocon_schema). -behaviour(hocon_schema).
@ -98,15 +99,20 @@ authenticate(#{password := Password} = Credential,
case emqx_resource:query(Unique, {sql, Query, Params}) of case emqx_resource:query(Unique, {sql, Query, Params}) of
{ok, _Columns, []} -> ignore; {ok, _Columns, []} -> ignore;
{ok, Columns, Rows} -> {ok, Columns, Rows} ->
%% TODO: Support superuser NColumns = [Name || #column{name = Name} <- Columns],
Selected = maps:from_list(lists:zip(Columns, Rows)), Selected = maps:from_list(lists:zip(NColumns, Rows)),
check_password(Password, Selected, State); case check_password(Password, Selected, State) of
ok ->
{ok, #{superuser => maps:get(<<"superuser">>, Selected, false)}};
{error, Reason} ->
{error, Reason}
end;
{error, _Reason} -> {error, _Reason} ->
ignore ignore
end end
catch catch
error:Reason -> error:Error ->
?LOG(warning, "The following error occurred in '~s' during authentication: ~p", [Unique, Reason]), ?LOG(warning, "The following error occurred in '~s' during authentication: ~p", [Unique, Error]),
ignore ignore
end. end.
@ -121,17 +127,17 @@ destroy(#{'_unique' := Unique}) ->
check_password(undefined, _Selected, _State) -> check_password(undefined, _Selected, _State) ->
{error, bad_username_or_password}; {error, bad_username_or_password};
check_password(Password, check_password(Password,
#{password_hash := Hash}, #{<<"password_hash">> := Hash},
#{password_hash_algorithm := bcrypt}) -> #{password_hash_algorithm := bcrypt}) ->
case {ok, Hash} =:= bcrypt:hashpw(Password, Hash) of case {ok, Hash} =:= bcrypt:hashpw(Password, Hash) of
true -> ok; true -> ok;
false -> {error, bad_username_or_password} false -> {error, bad_username_or_password}
end; end;
check_password(Password, check_password(Password,
#{password_hash := Hash} = Selected, #{<<"password_hash">> := Hash} = Selected,
#{password_hash_algorithm := Algorithm, #{password_hash_algorithm := Algorithm,
salt_position := SaltPosition}) -> salt_position := SaltPosition}) ->
Salt = maps:get(salt, Selected, <<>>), Salt = maps:get(<<"salt">>, Selected, <<>>),
case Hash =:= emqx_authn_utils:hash(Algorithm, Password, Salt, SaltPosition) of case Hash =:= emqx_authn_utils:hash(Algorithm, Password, Salt, SaltPosition) of
true -> ok; true -> ok;
false -> {error, bad_username_or_password} false -> {error, bad_username_or_password}

View File

@ -124,7 +124,13 @@ authenticate(#{password := Password} = Credential,
NKey = binary_to_list(iolist_to_binary(replace_placeholders(Key, Credential))), NKey = binary_to_list(iolist_to_binary(replace_placeholders(Key, Credential))),
case emqx_resource:query(Unique, {cmd, [Command, NKey | Fields]}) of case emqx_resource:query(Unique, {cmd, [Command, NKey | Fields]}) of
{ok, Values} -> {ok, Values} ->
check_password(Password, merge(Fields, Values), State); Selected = merge(Fields, Values),
case check_password(Password, Selected, State) of
ok ->
{ok, #{superuser => maps:get("superuser", Selected, false)}};
{error, Reason} ->
{error, Reason}
end;
{error, Reason} -> {error, Reason} ->
?LOG(error, "['~s'] Query failed: ~p", [Unique, Reason]), ?LOG(error, "['~s'] Query failed: ~p", [Unique, Reason]),
ignore ignore
@ -166,8 +172,8 @@ check_fields(["password_hash" | More], false) ->
check_fields(More, true); check_fields(More, true);
check_fields(["salt" | More], HasPassHash) -> check_fields(["salt" | More], HasPassHash) ->
check_fields(More, HasPassHash); check_fields(More, HasPassHash);
% check_fields(["is_superuser" | More], HasPassHash) -> check_fields(["superuser" | More], HasPassHash) ->
% check_fields(More, HasPassHash); check_fields(More, HasPassHash);
check_fields([Field | _], _) -> check_fields([Field | _], _) ->
error({unsupported_field, Field}). error({unsupported_field, Field}).

View File

@ -1,3 +1,3 @@
user_id,password_hash,salt user_id,password_hash,salt,superuser
myuser3,b6c743545a7817ae8c8f624371d5f5f0373234bb0ff36b8ffbf19bce0e06ab75,de1024f462fb83910fd13151bd4bd235 myuser3,b6c743545a7817ae8c8f624371d5f5f0373234bb0ff36b8ffbf19bce0e06ab75,de1024f462fb83910fd13151bd4bd235,true
myuser4,ee68c985a69208b6eda8c6c9b4c7c2d2b15ee2352cdd64a903171710a99182e8,ad773b5be9dd0613fe6c2f4d8c403139 myuser4,ee68c985a69208b6eda8c6c9b4c7c2d2b15ee2352cdd64a903171710a99182e8,ad773b5be9dd0613fe6c2f4d8c403139,false

1 user_id password_hash salt superuser
2 myuser3 b6c743545a7817ae8c8f624371d5f5f0373234bb0ff36b8ffbf19bce0e06ab75 de1024f462fb83910fd13151bd4bd235 true
3 myuser4 ee68c985a69208b6eda8c6c9b4c7c2d2b15ee2352cdd64a903171710a99182e8 ad773b5be9dd0613fe6c2f4d8c403139 false

View File

@ -2,11 +2,13 @@
{ {
"user_id":"myuser1", "user_id":"myuser1",
"password_hash":"c5e46903df45e5dc096dc74657610dbee8deaacae656df88a1788f1847390242", "password_hash":"c5e46903df45e5dc096dc74657610dbee8deaacae656df88a1788f1847390242",
"salt": "e378187547bf2d6f0545a3f441aa4d8a" "salt": "e378187547bf2d6f0545a3f441aa4d8a",
"superuser": true
}, },
{ {
"user_id":"myuser2", "user_id":"myuser2",
"password_hash":"f4d17f300b11e522fd33f497c11b126ef1ea5149c74d2220f9a16dc876d4567b", "password_hash":"f4d17f300b11e522fd33f497c11b126ef1ea5149c74d2220f9a16dc876d4567b",
"salt": "6d3f9bd5b54d94b98adbcfe10b6d181f" "salt": "6d3f9bd5b54d94b98adbcfe10b6d181f",
"superuser": false
} }
] ]

View File

@ -93,7 +93,7 @@ t_authenticator(_) ->
?assertMatch({ok, [#{name := AuthenticatorName1}, #{name := AuthenticatorName2}]}, ?AUTH:list_authenticators(?CHAIN)), ?assertMatch({ok, [#{name := AuthenticatorName1}, #{name := AuthenticatorName2}]}, ?AUTH:list_authenticators(?CHAIN)),
?assertEqual(ok, ?AUTH:move_authenticator(?CHAIN, ID2, {before, ID1})), ?assertEqual(ok, ?AUTH:move_authenticator(?CHAIN, ID2, {before, ID1})),
?assertMatch({ok, [#{name := AuthenticatorName2}, #{name := AuthenticatorName1}]}, ?AUTH:list_authenticators(?CHAIN)), ?assertMatch({ok, [#{name := AuthenticatorName2}, #{name := AuthenticatorName1}]}, ?AUTH:list_authenticators(?CHAIN)),
?assertEqual({error, {not_found, {authenticator, <<"nonexistent">>}}}, ?AUTH:move_authenticator(?CHAIN, ID2, {before, <<"nonexistent">>})), ?assertEqual({error, {not_found, {authenticator, <<"nonexistent">>}}}, ?AUTH:move_authenticator(?CHAIN, ID2, {before, <<"nonexistent">>})),
@ -108,7 +108,7 @@ t_authenticate(_) ->
listener => mqtt_tcp, listener => mqtt_tcp,
username => <<"myuser">>, username => <<"myuser">>,
password => <<"mypass">>}, password => <<"mypass">>},
?assertEqual(ok, emqx_access_control:authenticate(ClientInfo)), ?assertEqual({ok, #{superuser => false}}, emqx_access_control:authenticate(ClientInfo)),
?assertEqual(false, emqx_authn:is_enabled()), ?assertEqual(false, emqx_authn:is_enabled()),
emqx_authn:enable(), emqx_authn:enable(),
?assertEqual(true, emqx_authn:is_enabled()), ?assertEqual(true, emqx_authn:is_enabled()),

View File

@ -52,21 +52,27 @@ t_jwt_authenticator(_) ->
JWS = generate_jws('hmac-based', Payload, <<"abcdef">>), JWS = generate_jws('hmac-based', Payload, <<"abcdef">>),
ClientInfo = #{username => <<"myuser">>, ClientInfo = #{username => <<"myuser">>,
password => JWS}, password => JWS},
?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo, ok)), ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)),
Payload1 = #{<<"username">> => <<"myuser">>, <<"superuser">> => true},
JWS1 = generate_jws('hmac-based', Payload1, <<"abcdef">>),
ClientInfo1 = #{username => <<"myuser">>,
password => JWS1},
?assertEqual({stop, {ok, #{superuser => true}}}, ?AUTH:authenticate(ClientInfo1, ignored)),
BadJWS = generate_jws('hmac-based', Payload, <<"bad_secret">>), BadJWS = generate_jws('hmac-based', Payload, <<"bad_secret">>),
ClientInfo2 = ClientInfo#{password => BadJWS}, ClientInfo2 = ClientInfo#{password => BadJWS},
?assertEqual({stop, {error, not_authorized}}, ?AUTH:authenticate(ClientInfo2, ok)), ?assertEqual({stop, {error, not_authorized}}, ?AUTH:authenticate(ClientInfo2, ignored)),
%% secret_base64_encoded %% secret_base64_encoded
Config2 = Config#{secret => base64:encode(<<"abcdef">>), Config2 = Config#{secret => base64:encode(<<"abcdef">>),
secret_base64_encoded => true}, secret_base64_encoded => true},
?assertMatch({ok, _}, ?AUTH:update_authenticator(?CHAIN, ID, Config2)), ?assertMatch({ok, _}, ?AUTH:update_authenticator(?CHAIN, ID, Config2)),
?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo, ok)), ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)),
Config3 = Config#{verify_claims => [{<<"username">>, <<"${mqtt-username}">>}]}, Config3 = Config#{verify_claims => [{<<"username">>, <<"${mqtt-username}">>}]},
?assertMatch({ok, _}, ?AUTH:update_authenticator(?CHAIN, ID, Config3)), ?assertMatch({ok, _}, ?AUTH:update_authenticator(?CHAIN, ID, Config3)),
?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo, ok)), ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)),
?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo#{username => <<"otheruser">>}, ok)), ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo#{username => <<"otheruser">>}, ok)),
%% Expiration %% Expiration
@ -74,39 +80,39 @@ t_jwt_authenticator(_) ->
, <<"exp">> => erlang:system_time(second) - 60}, , <<"exp">> => erlang:system_time(second) - 60},
JWS3 = generate_jws('hmac-based', Payload3, <<"abcdef">>), JWS3 = generate_jws('hmac-based', Payload3, <<"abcdef">>),
ClientInfo3 = ClientInfo#{password => JWS3}, ClientInfo3 = ClientInfo#{password => JWS3},
?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo3, ok)), ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo3, ignored)),
Payload4 = #{ <<"username">> => <<"myuser">> Payload4 = #{ <<"username">> => <<"myuser">>
, <<"exp">> => erlang:system_time(second) + 60}, , <<"exp">> => erlang:system_time(second) + 60},
JWS4 = generate_jws('hmac-based', Payload4, <<"abcdef">>), JWS4 = generate_jws('hmac-based', Payload4, <<"abcdef">>),
ClientInfo4 = ClientInfo#{password => JWS4}, ClientInfo4 = ClientInfo#{password => JWS4},
?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo4, ok)), ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo4, ignored)),
%% Issued At %% Issued At
Payload5 = #{ <<"username">> => <<"myuser">> Payload5 = #{ <<"username">> => <<"myuser">>
, <<"iat">> => erlang:system_time(second) - 60}, , <<"iat">> => erlang:system_time(second) - 60},
JWS5 = generate_jws('hmac-based', Payload5, <<"abcdef">>), JWS5 = generate_jws('hmac-based', Payload5, <<"abcdef">>),
ClientInfo5 = ClientInfo#{password => JWS5}, ClientInfo5 = ClientInfo#{password => JWS5},
?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo5, ok)), ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo5, ignored)),
Payload6 = #{ <<"username">> => <<"myuser">> Payload6 = #{ <<"username">> => <<"myuser">>
, <<"iat">> => erlang:system_time(second) + 60}, , <<"iat">> => erlang:system_time(second) + 60},
JWS6 = generate_jws('hmac-based', Payload6, <<"abcdef">>), JWS6 = generate_jws('hmac-based', Payload6, <<"abcdef">>),
ClientInfo6 = ClientInfo#{password => JWS6}, ClientInfo6 = ClientInfo#{password => JWS6},
?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo6, ok)), ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo6, ignored)),
%% Not Before %% Not Before
Payload7 = #{ <<"username">> => <<"myuser">> Payload7 = #{ <<"username">> => <<"myuser">>
, <<"nbf">> => erlang:system_time(second) - 60}, , <<"nbf">> => erlang:system_time(second) - 60},
JWS7 = generate_jws('hmac-based', Payload7, <<"abcdef">>), JWS7 = generate_jws('hmac-based', Payload7, <<"abcdef">>),
ClientInfo7 = ClientInfo#{password => JWS7}, ClientInfo7 = ClientInfo#{password => JWS7},
?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo7, ok)), ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo7, ignored)),
Payload8 = #{ <<"username">> => <<"myuser">> Payload8 = #{ <<"username">> => <<"myuser">>
, <<"nbf">> => erlang:system_time(second) + 60}, , <<"nbf">> => erlang:system_time(second) + 60},
JWS8 = generate_jws('hmac-based', Payload8, <<"abcdef">>), JWS8 = generate_jws('hmac-based', Payload8, <<"abcdef">>),
ClientInfo8 = ClientInfo#{password => JWS8}, ClientInfo8 = ClientInfo#{password => JWS8},
?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo8, ok)), ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo8, ignored)),
?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID)), ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID)),
ok. ok.
@ -128,8 +134,8 @@ t_jwt_authenticator2(_) ->
JWS = generate_jws('public-key', Payload, PrivateKey), JWS = generate_jws('public-key', Payload, PrivateKey),
ClientInfo = #{username => <<"myuser">>, ClientInfo = #{username => <<"myuser">>,
password => JWS}, password => JWS},
?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo, ok)), ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)),
?assertEqual({stop, {error, not_authorized}}, ?AUTH:authenticate(ClientInfo#{password => <<"badpassword">>}, ok)), ?assertEqual({stop, {error, not_authorized}}, ?AUTH:authenticate(ClientInfo#{password => <<"badpassword">>}, ignored)),
?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID)), ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID)),
ok. ok.

View File

@ -50,33 +50,36 @@ t_mnesia_authenticator(_) ->
UserInfo = #{user_id => <<"myuser">>, UserInfo = #{user_id => <<"myuser">>,
password => <<"mypass">>}, password => <<"mypass">>},
?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:add_user(?CHAIN, ID, UserInfo)), ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:add_user(?CHAIN, ID, UserInfo)),
?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser">>)), ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser">>)),
ClientInfo = #{zone => external, ClientInfo = #{zone => external,
username => <<"myuser">>, username => <<"myuser">>,
password => <<"mypass">>}, password => <<"mypass">>},
?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo, ok)), ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)),
?AUTH:enable(), ?AUTH:enable(),
?assertEqual(ok, emqx_access_control:authenticate(ClientInfo)), ?assertEqual({ok, #{superuser => false}}, emqx_access_control:authenticate(ClientInfo)),
ClientInfo2 = ClientInfo#{username => <<"baduser">>}, ClientInfo2 = ClientInfo#{username => <<"baduser">>},
?assertEqual({stop, {error, not_authorized}}, ?AUTH:authenticate(ClientInfo2, ok)), ?assertEqual({stop, {error, not_authorized}}, ?AUTH:authenticate(ClientInfo2, ignored)),
?assertEqual({error, not_authorized}, emqx_access_control:authenticate(ClientInfo2)), ?assertEqual({error, not_authorized}, emqx_access_control:authenticate(ClientInfo2)),
ClientInfo3 = ClientInfo#{password => <<"badpass">>}, ClientInfo3 = ClientInfo#{password => <<"badpass">>},
?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo3, ok)), ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo3, ignored)),
?assertEqual({error, bad_username_or_password}, emqx_access_control:authenticate(ClientInfo3)), ?assertEqual({error, bad_username_or_password}, emqx_access_control:authenticate(ClientInfo3)),
UserInfo2 = UserInfo#{password => <<"mypass2">>}, UserInfo2 = UserInfo#{password => <<"mypass2">>},
?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:update_user(?CHAIN, ID, <<"myuser">>, UserInfo2)), ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:update_user(?CHAIN, ID, <<"myuser">>, UserInfo2)),
ClientInfo4 = ClientInfo#{password => <<"mypass2">>}, ClientInfo4 = ClientInfo#{password => <<"mypass2">>},
?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo4, ok)), ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo4, ignored)),
?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:update_user(?CHAIN, ID, <<"myuser">>, #{superuser => true})),
?assertEqual({stop, {ok, #{superuser => true}}}, ?AUTH:authenticate(ClientInfo4, ignored)),
?assertEqual(ok, ?AUTH:delete_user(?CHAIN, ID, <<"myuser">>)), ?assertEqual(ok, ?AUTH:delete_user(?CHAIN, ID, <<"myuser">>)),
?assertEqual({error, not_found}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser">>)), ?assertEqual({error, not_found}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser">>)),
?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:add_user(?CHAIN, ID, UserInfo)), ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:add_user(?CHAIN, ID, UserInfo)),
?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser">>)), ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser">>)),
?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID)), ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID)),
@ -104,10 +107,16 @@ t_import(_) ->
ClientInfo1 = #{username => <<"myuser1">>, ClientInfo1 = #{username => <<"myuser1">>,
password => <<"mypassword1">>}, password => <<"mypassword1">>},
?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo1, ok)), ?assertEqual({stop, {ok, #{superuser => true}}}, ?AUTH:authenticate(ClientInfo1, ignored)),
ClientInfo2 = ClientInfo1#{username => <<"myuser3">>,
ClientInfo2 = ClientInfo1#{username => <<"myuser2">>,
password => <<"mypassword2">>},
?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo2, ignored)),
ClientInfo3 = ClientInfo1#{username => <<"myuser3">>,
password => <<"mypassword3">>}, password => <<"mypassword3">>},
?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo2, ok)), ?assertEqual({stop, {ok, #{superuser => true}}}, ?AUTH:authenticate(ClientInfo3, ignored)),
?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID)), ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID)),
ok. ok.
@ -131,11 +140,11 @@ t_multi_mnesia_authenticator(_) ->
{ok, #{name := AuthenticatorName1, id := ID1}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig1), {ok, #{name := AuthenticatorName1, id := ID1}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig1),
{ok, #{name := AuthenticatorName2, id := ID2}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig2), {ok, #{name := AuthenticatorName2, id := ID2}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig2),
?assertEqual({ok, #{user_id => <<"myuser">>}}, ?assertMatch({ok, #{user_id := <<"myuser">>}},
?AUTH:add_user(?CHAIN, ID1, ?AUTH:add_user(?CHAIN, ID1,
#{user_id => <<"myuser">>, #{user_id => <<"myuser">>,
password => <<"mypass1">>})), password => <<"mypass1">>})),
?assertEqual({ok, #{user_id => <<"myclient">>}}, ?assertMatch({ok, #{user_id := <<"myclient">>}},
?AUTH:add_user(?CHAIN, ID2, ?AUTH:add_user(?CHAIN, ID2,
#{user_id => <<"myclient">>, #{user_id => <<"myclient">>,
password => <<"mypass2">>})), password => <<"mypass2">>})),
@ -143,12 +152,12 @@ t_multi_mnesia_authenticator(_) ->
ClientInfo1 = #{username => <<"myuser">>, ClientInfo1 = #{username => <<"myuser">>,
clientid => <<"myclient">>, clientid => <<"myclient">>,
password => <<"mypass1">>}, password => <<"mypass1">>},
?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo1, ok)), ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo1, ignored)),
?assertEqual(ok, ?AUTH:move_authenticator(?CHAIN, ID2, top)), ?assertEqual(ok, ?AUTH:move_authenticator(?CHAIN, ID2, top)),
?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo1, ok)), ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo1, ignored)),
ClientInfo2 = ClientInfo1#{password => <<"mypass2">>}, ClientInfo2 = ClientInfo1#{password => <<"mypass2">>},
?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo2, ok)), ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo2, ignored)),
?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID1)), ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID1)),
?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID2)), ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID2)),