From e6f9767066ebc651ea3de40543fd943b541971a3 Mon Sep 17 00:00:00 2001 From: zhouzb Date: Fri, 13 Aug 2021 10:32:31 +0800 Subject: [PATCH] feat(authn): support superuser --- apps/emqx/src/emqx_access_control.erl | 9 +- apps/emqx/src/emqx_channel.erl | 19 ++-- apps/emqx_authn/data/user-credentials.csv | 6 +- apps/emqx_authn/data/user-credentials.json | 6 +- apps/emqx_authn/src/emqx_authn.erl | 5 +- apps/emqx_authn/src/emqx_authn_api.erl | 42 ++++++--- .../emqx_enhanced_authn_scram_mnesia.erl | 88 +++++++++++++------ .../src/simple_authn/emqx_authn_http.erl | 13 +-- .../src/simple_authn/emqx_authn_jwt.erl | 9 +- .../src/simple_authn/emqx_authn_mnesia.erl | 75 +++++++++------- .../src/simple_authn/emqx_authn_mongodb.erl | 8 +- .../src/simple_authn/emqx_authn_mysql.erl | 18 ++-- .../src/simple_authn/emqx_authn_pgsql.erl | 22 +++-- .../src/simple_authn/emqx_authn_redis.erl | 12 ++- .../emqx_authn/test/data/user-credentials.csv | 6 +- .../test/data/user-credentials.json | 6 +- apps/emqx_authn/test/emqx_authn_SUITE.erl | 4 +- apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl | 30 ++++--- .../test/emqx_authn_mnesia_SUITE.erl | 43 +++++---- 19 files changed, 270 insertions(+), 151 deletions(-) diff --git a/apps/emqx/src/emqx_access_control.erl b/apps/emqx/src/emqx_access_control.erl index 65991d222..111a86112 100644 --- a/apps/emqx/src/emqx_access_control.erl +++ b/apps/emqx/src/emqx_access_control.erl @@ -27,9 +27,14 @@ %%-------------------------------------------------------------------- -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) -> - 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 -spec authorize(emqx_types:clientinfo(), emqx_types:pubsub(), emqx_types:topic()) diff --git a/apps/emqx/src/emqx_channel.erl b/apps/emqx/src/emqx_channel.erl index 5e4d11953..93d5e2c37 100644 --- a/apps/emqx/src/emqx_channel.erl +++ b/apps/emqx/src/emqx_channel.erl @@ -1299,14 +1299,17 @@ authenticate(?AUTH_PACKET(_, #{'Authentication-Method' := AuthMethod} = Properti {error, ?RC_BAD_AUTHENTICATION_METHOD} end. -do_authenticate(#{auth_method := AuthMethod} = Credential, Channel) -> +do_authenticate(#{auth_method := AuthMethod} = Credential, #channel{clientinfo = ClientInfo} = Channel) -> Properties = #{'Authentication-Method' => AuthMethod}, case emqx_access_control:authenticate(Credential) of - ok -> - {ok, Properties, Channel#channel{auth_cache = #{}}}; - {ok, AuthData} -> + {ok, #{superuser := Superuser}} -> + {ok, Properties, + Channel#channel{clientinfo = ClientInfo#{is_superuser => Superuser}, + auth_cache = #{}}}; + {ok, #{superuser := Superuser}, AuthData} -> {ok, Properties#{'Authentication-Data' => AuthData}, - Channel#channel{auth_cache = #{}}}; + Channel#channel{clientinfo = ClientInfo#{is_superuser => Superuser}, + auth_cache = #{}}}; {continue, AuthCache} -> {continue, Properties, Channel#channel{auth_cache = AuthCache}}; {continue, AuthData, AuthCache} -> @@ -1316,10 +1319,10 @@ do_authenticate(#{auth_method := AuthMethod} = Credential, Channel) -> {error, emqx_reason_codes:connack_error(Reason)} end; -do_authenticate(Credential, Channel) -> +do_authenticate(Credential, #channel{clientinfo = ClientInfo} = Channel) -> case emqx_access_control:authenticate(Credential) of - ok -> - {ok, #{}, Channel}; + {ok, #{superuser := Superuser}} -> + {ok, #{}, Channel#channel{clientinfo = ClientInfo#{is_superuser => Superuser}}}; {error, Reason} -> {error, emqx_reason_codes:connack_error(Reason)} end. diff --git a/apps/emqx_authn/data/user-credentials.csv b/apps/emqx_authn/data/user-credentials.csv index 2543d39ca..0548308b7 100644 --- a/apps/emqx_authn/data/user-credentials.csv +++ b/apps/emqx_authn/data/user-credentials.csv @@ -1,3 +1,3 @@ -user_id,password_hash,salt -myuser3,b6c743545a7817ae8c8f624371d5f5f0373234bb0ff36b8ffbf19bce0e06ab75,de1024f462fb83910fd13151bd4bd235 -myuser4,ee68c985a69208b6eda8c6c9b4c7c2d2b15ee2352cdd64a903171710a99182e8,ad773b5be9dd0613fe6c2f4d8c403139 +user_id,password_hash,salt,superuser +myuser3,b6c743545a7817ae8c8f624371d5f5f0373234bb0ff36b8ffbf19bce0e06ab75,de1024f462fb83910fd13151bd4bd235,true +myuser4,ee68c985a69208b6eda8c6c9b4c7c2d2b15ee2352cdd64a903171710a99182e8,ad773b5be9dd0613fe6c2f4d8c403139,false diff --git a/apps/emqx_authn/data/user-credentials.json b/apps/emqx_authn/data/user-credentials.json index 169122bd2..e54501233 100644 --- a/apps/emqx_authn/data/user-credentials.json +++ b/apps/emqx_authn/data/user-credentials.json @@ -2,11 +2,13 @@ { "user_id":"myuser1", "password_hash":"c5e46903df45e5dc096dc74657610dbee8deaacae656df88a1788f1847390242", - "salt": "e378187547bf2d6f0545a3f441aa4d8a" + "salt": "e378187547bf2d6f0545a3f441aa4d8a", + "superuser": true }, { "user_id":"myuser2", "password_hash":"f4d17f300b11e522fd33f497c11b126ef1ea5149c74d2220f9a16dc876d4567b", - "salt": "6d3f9bd5b54d94b98adbcfe10b6d181f" + "salt": "6d3f9bd5b54d94b98adbcfe10b6d181f", + "superuser": false } ] diff --git a/apps/emqx_authn/src/emqx_authn.erl b/apps/emqx_authn/src/emqx_authn.erl index 90114c269..384830750 100644 --- a/apps/emqx_authn/src/emqx_authn.erl +++ b/apps/emqx_authn/src/emqx_authn.erl @@ -235,8 +235,9 @@ do_authenticate([{_, _, #authenticator{provider = Provider, state = State}} | Mo ignore -> do_authenticate(More, Credential); Result -> - %% ok - %% {ok, AuthData} + %% {ok, Extra} + %% {ok, Extra, AuthData} + %% {ok, MetaData} %% {continue, AuthCache} %% {continue, AuthData, AuthCache} %% {error, Reason} diff --git a/apps/emqx_authn/src/emqx_authn_api.erl b/apps/emqx_authn/src/emqx_authn_api.erl index bcaaebe93..97ed94a8b 100644 --- a/apps/emqx_authn/src/emqx_authn_api.erl +++ b/apps/emqx_authn/src/emqx_authn_api.erl @@ -528,6 +528,10 @@ users_api() -> }, password => #{ type => string + }, + superuser => #{ + type => boolean, + default => false } } } @@ -541,10 +545,12 @@ users_api() -> 'application/json' => #{ schema => #{ type => object, - required => [user_id], properties => #{ user_id => #{ type => string + }, + superuser => #{ + type => boolean } } } @@ -576,10 +582,12 @@ users_api() -> type => array, items => #{ type => object, - required => [user_id], properties => #{ user_id => #{ type => string + }, + superuser => #{ + type => boolean } } } @@ -620,10 +628,12 @@ users2_api() -> 'application/json' => #{ schema => #{ type => object, - required => [password], properties => #{ password => #{ type => string + }, + superuser => #{ + type => boolean } } } @@ -642,6 +652,9 @@ users2_api() -> properties => #{ user_id => #{ type => string + }, + superuser => #{ + type => boolean } } } @@ -685,6 +698,9 @@ users2_api() -> properties => #{ user_id => #{ type => string + }, + superuser => #{ + type => boolean } } } @@ -1359,9 +1375,11 @@ users(post, Request) -> {ok, Body, _} = cowboy_req:read_body(Request), case emqx_json:decode(Body, [return_maps]) of #{ <<"user_id">> := UserID - , <<"password">> := Password} -> + , <<"password">> := Password} = UserInfo -> + Superuser = maps:get(<<"superuser">>, UserInfo, false), case emqx_authn:add_user(?CHAIN, AuthenticatorID, #{ user_id => UserID - , password => Password}) of + , password => Password + , superuser => Superuser}) of {ok, User} -> {201, User}; {error, Reason} -> @@ -1385,16 +1403,18 @@ users2(patch, Request) -> AuthenticatorID = cowboy_req:binding(id, Request), UserID = cowboy_req:binding(user_id, Request), {ok, Body, _} = cowboy_req:read_body(Request), - case emqx_json:decode(Body, [return_maps]) of - #{<<"password">> := Password} -> - case emqx_authn:update_user(?CHAIN, AuthenticatorID, UserID, #{password => Password}) of + UserInfo = emqx_json:decode(Body, [return_maps]), + NUserInfo = maps:with([<<"password">>, <<"superuser">>], UserInfo), + case NUserInfo =:= #{} of + true -> + serialize_error({missing_parameter, password}); + false -> + case emqx_authn:update_user(?CHAIN, AuthenticatorID, UserID, UserInfo) of {ok, User} -> {200, User}; {error, Reason} -> serialize_error(Reason) - end; - _ -> - serialize_error({missing_parameter, password}) + end end; users2(get, Request) -> AuthenticatorID = cowboy_req:binding(id, Request), diff --git a/apps/emqx_authn/src/enhanced_authn/emqx_enhanced_authn_scram_mnesia.erl b/apps/emqx_authn/src/enhanced_authn/emqx_enhanced_authn_scram_mnesia.erl index 56629c568..98cdb8c26 100644 --- a/apps/emqx_authn/src/enhanced_authn/emqx_enhanced_authn_scram_mnesia.erl +++ b/apps/emqx_authn/src/enhanced_authn/emqx_enhanced_authn_scram_mnesia.erl @@ -17,7 +17,6 @@ -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). @@ -48,6 +47,14 @@ -rlog_shard({?AUTH_SHARD, ?TAB}). +-record(user_info, + { user_id + , stored_key + , server_key + , salt + , superuser + }). + %%------------------------------------------------------------------------------ %% Mnesia bootstrap %%------------------------------------------------------------------------------ @@ -57,8 +64,8 @@ mnesia(boot) -> ok = ekka_mnesia:create_table(?TAB, [ {disc_copies, [node()]}, - {record_name, scram_user_credentail}, - {attributes, record_info(fields, scram_user_credentail)}, + {record_name, user_info}, + {attributes, record_info(fields, user_info)}, {storage_properties, [{ets, [{read_concurrency, true}]}]}]); mnesia(copy) -> @@ -126,20 +133,21 @@ authenticate(_Credential, _State) -> destroy(#{user_group := UserGroup}) -> trans( fun() -> - MatchSpec = [{{scram_user_credentail, {UserGroup, '_'}, '_', '_', '_'}, [], ['$_']}], - ok = lists:foreach(fun(UserCredential) -> - mnesia:delete_object(?TAB, UserCredential, write) + MatchSpec = [{{user_info, {UserGroup, '_'}, '_', '_', '_', '_'}, [], ['$_']}], + ok = lists:foreach(fun(UserInfo) -> + mnesia:delete_object(?TAB, UserInfo, write) end, mnesia:select(?TAB, MatchSpec, write)) end). add_user(#{user_id := UserID, - password := Password}, #{user_group := UserGroup} = State) -> + password := Password} = UserInfo, #{user_group := UserGroup} = State) -> trans( fun() -> case mnesia:read(?TAB, {UserGroup, UserID}, write) of [] -> - add_user(UserID, Password, State), - {ok, #{user_id => UserID}}; + Superuser = maps:get(superuser, UserInfo, false), + add_user(UserID, Password, Superuser, State), + {ok, #{user_id => UserID, superuser => Superuser}}; [_] -> {error, already_exist} end @@ -156,31 +164,41 @@ delete_user(UserID, #{user_group := UserGroup}) -> end end). -update_user(UserID, #{password := Password}, +update_user(UserID, User, #{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}} + [#user_info{superuser = Superuser} = UserInfo] -> + UserInfo1 = UserInfo#user_info{superuser = maps:get(superuser, User, Superuser)}, + 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). lookup_user(UserID, #{user_group := UserGroup}) -> case mnesia:dirty_read(?TAB, {UserGroup, UserID}) of - [#scram_user_credentail{user_id = {_, UserID}}] -> - {ok, #{user_id => UserID}}; + [UserInfo] -> + {ok, serialize_user_info(UserInfo)}; [] -> {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], + Users = [serialize_user_info(UserInfo) || + #user_info{user_id = {UserGroup0, _}} = UserInfo <- ets:tab2list(?TAB), UserGroup0 =:= UserGroup], {ok, Users}. %%------------------------------------------------------------------------------ @@ -195,13 +213,13 @@ ensure_auth_method(_, _) -> false. check_client_first_message(Bin, _Cache, #{iteration_count := IterationCount} = State) -> - LookupFun = fun(Username) -> - lookup_user2(Username, State) + RetrieveFun = fun(Username) -> + retrieve(Username, State) end, case esasl_scram:check_client_first_message( Bin, #{iteration_count => IterationCount, - lookup => LookupFun} + retrieve => RetrieveFun} ) of {cotinue, ServerFirstMessage, Cache} -> {cotinue, ServerFirstMessage, Cache}; @@ -209,25 +227,36 @@ check_client_first_message(Bin, _Cache, #{iteration_count := IterationCount} = S {error, not_authorized} 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( Bin, Cache#{algorithm => Alg} ) of {ok, ServerFinalMessage} -> - {ok, ServerFinalMessage}; + {ok, #{superuser => Superuser}, 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). +add_user(UserID, Password, Superuser, State) -> + {StoredKey, ServerKey, Salt} = esasl_scram:generate_user_credential(Password, State), + 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 - [#scram_user_credentail{} = UserCredential] -> - {ok, UserCredential}; + [#user_info{stored_key = StoredKey, + server_key = ServerKey, + salt = Salt, + superuser = Superuser}] -> + {ok, #{stored_key => StoredKey, + server_key => ServerKey, + salt => Salt, + superuser => Superuser}}; [] -> {error, not_found} end. @@ -241,3 +270,6 @@ trans(Fun, Args) -> {atomic, Res} -> Res; {aborted, Reason} -> {error, Reason} end. + +serialize_user_info(#user_info{user_id = {_, UserID}, superuser = Superuser}) -> + #{user_id => UserID, superuser => Superuser}. \ No newline at end of file diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl index aa10a3b98..026df2415 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl @@ -154,15 +154,16 @@ authenticate(Credential, #{'_unique' := Unique, try Request = generate_request(Credential, State), case emqx_resource:query(Unique, {Method, Request, RequestTimeout}) of - {ok, 204, _Headers} -> ok; + {ok, 204, _Headers} -> {ok, #{superuser => false}}; {ok, 200, Headers, Body} -> ContentType = proplists:get_value(<<"content-type">>, Headers, <<"application/json">>), case safely_parse_body(ContentType, Body) of - {ok, _NBody} -> + {ok, NBody} -> %% TODO: Return by user property - ok; + {ok, #{superuser => maps:get(<<"superuser">>, NBody, false), + user_property => NBody}}; {error, _Reason} -> - ok + {ok, #{superuser => false}} end; {error, _Reason} -> ignore @@ -291,8 +292,8 @@ safely_parse_body(ContentType, Body) -> end. 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) -> - {ok, cow_qs:parse_qs(Body)}; + {ok, maps:from_list(cow_qs:parse_qs(Body))}; parse_body(ContentType, _) -> {error, {unsupported_content_type, ContentType}}. \ No newline at end of file diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl index fe034994e..74aa9e8f6 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_jwt.erl @@ -169,7 +169,7 @@ authenticate(Credential = #{password := JWT}, #{jwk := JWK, end, VerifyClaims = replace_placeholder(VerifyClaims0, Credential), case verify(JWT, JWKs, VerifyClaims) of - ok -> ok; + {ok, Extra} -> {ok, Extra}; {error, invalid_signature} -> ignore; {error, {claims, _}} -> {error, bad_username_or_password} end. @@ -239,7 +239,12 @@ verify(JWS, [JWK | More], VerifyClaims) -> try jose_jws:verify(JWK, JWS) of {true, Payload, _JWS} -> 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, _, _} -> verify(JWS, More, VerifyClaims) catch diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl index ce845d4e3..08c0ffad1 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl @@ -46,6 +46,7 @@ { user_id :: {user_group(), user_id()} , password_hash :: binary() , salt :: binary() + , superuser :: boolean() }). -reflect_type([ user_id_type/0 ]). @@ -147,13 +148,13 @@ authenticate(#{password := Password} = Credential, case mnesia:dirty_read(?TAB, {UserGroup, UserID}) of [] -> ignore; - [#user_info{password_hash = PasswordHash, salt = Salt0}] -> + [#user_info{password_hash = PasswordHash, salt = Salt0, superuser = Superuser}] -> Salt = case Algorithm of bcrypt -> PasswordHash; _ -> Salt0 end, case PasswordHash =:= hash(Algorithm, Password, Salt) of - true -> ok; + true -> {ok, #{superuser => Superuser}}; false -> {error, bad_username_or_password} end end. @@ -161,7 +162,7 @@ authenticate(#{password := Password} = Credential, destroy(#{user_group := UserGroup}) -> trans( fun() -> - MatchSpec = [{{user_info, {UserGroup, '_'}, '_', '_'}, [], ['$_']}], + MatchSpec = [{{user_info, {UserGroup, '_'}, '_', '_', '_'}, [], ['$_']}], ok = lists:foreach(fun delete_user2/1, mnesia:select(?TAB, MatchSpec, write)) end). @@ -179,14 +180,16 @@ import_users(Filename0, State) -> end. add_user(#{user_id := UserID, - password := Password}, + password := Password} = UserInfo, #{user_group := UserGroup} = State) -> trans( fun() -> case mnesia:read(?TAB, {UserGroup, UserID}, write) of [] -> - add(UserID, Password, State), - {ok, #{user_id => UserID}}; + {PasswordHash, Salt} = hash(Password, State), + Superuser = maps:get(superuser, UserInfo, false), + insert_user(UserGroup, UserID, PasswordHash, Salt, Superuser), + {ok, #{user_id => UserID, superuser => Superuser}}; [_] -> {error, already_exist} end @@ -203,29 +206,38 @@ delete_user(UserID, #{user_group := UserGroup}) -> end end). -update_user(UserID, #{password := Password}, +update_user(UserID, UserInfo, #{user_group := UserGroup} = State) -> trans( fun() -> case mnesia:read(?TAB, {UserGroup, UserID}, write) of [] -> {error, not_found}; - [_] -> - add(UserID, Password, State), - {ok, #{user_id => UserID}} + [#user_info{ password_hash = PasswordHash + , salt = Salt + , 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). lookup_user(UserID, #{user_group := UserGroup}) -> case mnesia:dirty_read(?TAB, {UserGroup, UserID}) of - [#user_info{user_id = {_, UserID}}] -> - {ok, #{user_id => UserID}}; + [UserInfo] -> + {ok, serialize_user_info(UserInfo)}; [] -> {error, not_found} end. 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}. %%------------------------------------------------------------------------------ @@ -268,7 +280,8 @@ import(UserGroup, [#{<<"user_id">> := UserID, <<"password_hash">> := PasswordHash} = UserInfo | More]) when is_binary(UserID) andalso is_binary(PasswordHash) -> 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]) -> {error, bad_format}. @@ -282,7 +295,8 @@ import(UserGroup, File, Seq) -> {ok, #{user_id := UserID, password_hash := PasswordHash} = 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); {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([], [], #{user_id := _, password_hash := _, salt := _} = Acc) -> - {ok, Acc}; get_user_info_by_seq([], [], #{user_id := _, password_hash := _} = Acc) -> {ok, Acc}; 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([Salt | More1], [<<"salt">> | More2], Acc) -> 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(_, _, _) -> {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 := bcrypt, @@ -347,13 +353,16 @@ hash(bcrypt, Password, Salt) -> hash(Algorithm, Password, Salt) -> emqx_passwd:hash(Algorithm, <>). -insert_user(UserGroup, UserID, PasswordHash) -> - insert_user(UserGroup, UserID, PasswordHash, <<>>). +hash(Password, #{password_hash_algorithm := Algorithm} = State) -> + 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}, password_hash = PasswordHash, - salt = Salt}, + salt = Salt, + superuser = Superuser}, mnesia:write(?TAB, UserInfo, write). delete_user2(UserInfo) -> @@ -376,8 +385,10 @@ trans(Fun, Args) -> {aborted, Reason} -> {error, Reason} end. - to_binary(B) when is_binary(B) -> B; to_binary(L) when is_list(L) -> iolist_to_binary(L). + +serialize_user_info(#user_info{user_id = {_, UserID}, superuser = Superuser}) -> + #{user_id => UserID, superuser => Superuser}. \ No newline at end of file diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl index ff1b2161a..56ced0104 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_mongodb.erl @@ -140,7 +140,8 @@ authenticate(#{password := Password} = Credential, ignore; Doc -> case check_password(Password, Doc, State) of - ok -> ok; + ok -> + {ok, #{superuser => superuser(Doc, State)}}; {error, {cannot_find_password_hash_field, PasswordHashField}} -> ?LOG(error, "['~s'] Can't find password hash field: ~s", [Unique, PasswordHashField]), {error, bad_username_or_password}; @@ -221,6 +222,11 @@ check_password(Password, end end. +superuser(Doc, #{superuser_field := SuperuserField}) -> + maps:get(SuperuserField, Doc, false); +superuser(_, _) -> + false. + hash(Algorithm, Password, Salt, prefix) -> emqx_passwd:hash(Algorithm, <>); hash(Algorithm, Password, Salt, suffix) -> diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl index f2a01e7e1..75a3392ec 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_mysql.erl @@ -112,15 +112,19 @@ authenticate(#{password := Password} = Credential, case emqx_resource:query(Unique, {sql, Query, Params, Timeout}) of {ok, _Columns, []} -> ignore; {ok, Columns, Rows} -> - %% TODO: Support superuser 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} -> ignore end catch - error:Reason -> - ?LOG(warning, "The following error occurred in '~s' during authentication: ~p", [Unique, Reason]), + error:Error -> + ?LOG(warning, "The following error occurred in '~s' during authentication: ~p", [Unique, Error]), ignore end. @@ -135,17 +139,17 @@ destroy(#{'_unique' := Unique}) -> check_password(undefined, _Selected, _State) -> {error, bad_username_or_password}; check_password(Password, - #{password_hash := Hash}, + #{<<"password_hash">> := Hash}, #{password_hash_algorithm := bcrypt}) -> case {ok, Hash} =:= bcrypt:hashpw(Password, Hash) of true -> ok; false -> {error, bad_username_or_password} end; check_password(Password, - #{password_hash := Hash} = Selected, + #{<<"password_hash">> := Hash} = Selected, #{password_hash_algorithm := Algorithm, salt_position := SaltPosition}) -> - Salt = maps:get(salt, Selected, <<>>), + Salt = maps:get(<<"salt">>, Selected, <<>>), case Hash =:= emqx_authn_utils:hash(Algorithm, Password, Salt, SaltPosition) of true -> ok; false -> {error, bad_username_or_password} diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_pgsql.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_pgsql.erl index b83e111c3..44c7f7185 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_pgsql.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_pgsql.erl @@ -18,6 +18,7 @@ -include("emqx_authn.hrl"). -include_lib("emqx/include/logger.hrl"). +-include_lib("epgsql/include/epgsql.hrl"). -include_lib("typerefl/include/types.hrl"). -behaviour(hocon_schema). @@ -98,15 +99,20 @@ authenticate(#{password := Password} = Credential, case emqx_resource:query(Unique, {sql, Query, Params}) of {ok, _Columns, []} -> ignore; {ok, Columns, Rows} -> - %% TODO: Support superuser - Selected = maps:from_list(lists:zip(Columns, Rows)), - check_password(Password, Selected, State); + NColumns = [Name || #column{name = Name} <- Columns], + Selected = maps:from_list(lists:zip(NColumns, Rows)), + case check_password(Password, Selected, State) of + ok -> + {ok, #{superuser => maps:get(<<"superuser">>, Selected, false)}}; + {error, Reason} -> + {error, Reason} + end; {error, _Reason} -> ignore end catch - error:Reason -> - ?LOG(warning, "The following error occurred in '~s' during authentication: ~p", [Unique, Reason]), + error:Error -> + ?LOG(warning, "The following error occurred in '~s' during authentication: ~p", [Unique, Error]), ignore end. @@ -121,17 +127,17 @@ destroy(#{'_unique' := Unique}) -> check_password(undefined, _Selected, _State) -> {error, bad_username_or_password}; check_password(Password, - #{password_hash := Hash}, + #{<<"password_hash">> := Hash}, #{password_hash_algorithm := bcrypt}) -> case {ok, Hash} =:= bcrypt:hashpw(Password, Hash) of true -> ok; false -> {error, bad_username_or_password} end; check_password(Password, - #{password_hash := Hash} = Selected, + #{<<"password_hash">> := Hash} = Selected, #{password_hash_algorithm := Algorithm, salt_position := SaltPosition}) -> - Salt = maps:get(salt, Selected, <<>>), + Salt = maps:get(<<"salt">>, Selected, <<>>), case Hash =:= emqx_authn_utils:hash(Algorithm, Password, Salt, SaltPosition) of true -> ok; false -> {error, bad_username_or_password} diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_redis.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_redis.erl index 5d6e579ac..0c2696c0e 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_redis.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_redis.erl @@ -124,7 +124,13 @@ authenticate(#{password := Password} = Credential, NKey = binary_to_list(iolist_to_binary(replace_placeholders(Key, Credential))), case emqx_resource:query(Unique, {cmd, [Command, NKey | Fields]}) of {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} -> ?LOG(error, "['~s'] Query failed: ~p", [Unique, Reason]), ignore @@ -166,8 +172,8 @@ check_fields(["password_hash" | More], false) -> check_fields(More, true); check_fields(["salt" | More], HasPassHash) -> check_fields(More, HasPassHash); -% check_fields(["is_superuser" | More], HasPassHash) -> -% check_fields(More, HasPassHash); +check_fields(["superuser" | More], HasPassHash) -> + check_fields(More, HasPassHash); check_fields([Field | _], _) -> error({unsupported_field, Field}). diff --git a/apps/emqx_authn/test/data/user-credentials.csv b/apps/emqx_authn/test/data/user-credentials.csv index 2543d39ca..0548308b7 100644 --- a/apps/emqx_authn/test/data/user-credentials.csv +++ b/apps/emqx_authn/test/data/user-credentials.csv @@ -1,3 +1,3 @@ -user_id,password_hash,salt -myuser3,b6c743545a7817ae8c8f624371d5f5f0373234bb0ff36b8ffbf19bce0e06ab75,de1024f462fb83910fd13151bd4bd235 -myuser4,ee68c985a69208b6eda8c6c9b4c7c2d2b15ee2352cdd64a903171710a99182e8,ad773b5be9dd0613fe6c2f4d8c403139 +user_id,password_hash,salt,superuser +myuser3,b6c743545a7817ae8c8f624371d5f5f0373234bb0ff36b8ffbf19bce0e06ab75,de1024f462fb83910fd13151bd4bd235,true +myuser4,ee68c985a69208b6eda8c6c9b4c7c2d2b15ee2352cdd64a903171710a99182e8,ad773b5be9dd0613fe6c2f4d8c403139,false diff --git a/apps/emqx_authn/test/data/user-credentials.json b/apps/emqx_authn/test/data/user-credentials.json index 169122bd2..e54501233 100644 --- a/apps/emqx_authn/test/data/user-credentials.json +++ b/apps/emqx_authn/test/data/user-credentials.json @@ -2,11 +2,13 @@ { "user_id":"myuser1", "password_hash":"c5e46903df45e5dc096dc74657610dbee8deaacae656df88a1788f1847390242", - "salt": "e378187547bf2d6f0545a3f441aa4d8a" + "salt": "e378187547bf2d6f0545a3f441aa4d8a", + "superuser": true }, { "user_id":"myuser2", "password_hash":"f4d17f300b11e522fd33f497c11b126ef1ea5149c74d2220f9a16dc876d4567b", - "salt": "6d3f9bd5b54d94b98adbcfe10b6d181f" + "salt": "6d3f9bd5b54d94b98adbcfe10b6d181f", + "superuser": false } ] diff --git a/apps/emqx_authn/test/emqx_authn_SUITE.erl b/apps/emqx_authn/test/emqx_authn_SUITE.erl index 9c4371838..bf6447aba 100644 --- a/apps/emqx_authn/test/emqx_authn_SUITE.erl +++ b/apps/emqx_authn/test/emqx_authn_SUITE.erl @@ -93,7 +93,7 @@ t_authenticator(_) -> ?assertMatch({ok, [#{name := AuthenticatorName1}, #{name := AuthenticatorName2}]}, ?AUTH:list_authenticators(?CHAIN)), ?assertEqual(ok, ?AUTH:move_authenticator(?CHAIN, ID2, {before, ID1})), - + ?assertMatch({ok, [#{name := AuthenticatorName2}, #{name := AuthenticatorName1}]}, ?AUTH:list_authenticators(?CHAIN)), ?assertEqual({error, {not_found, {authenticator, <<"nonexistent">>}}}, ?AUTH:move_authenticator(?CHAIN, ID2, {before, <<"nonexistent">>})), @@ -108,7 +108,7 @@ t_authenticate(_) -> listener => mqtt_tcp, username => <<"myuser">>, password => <<"mypass">>}, - ?assertEqual(ok, emqx_access_control:authenticate(ClientInfo)), + ?assertEqual({ok, #{superuser => false}}, emqx_access_control:authenticate(ClientInfo)), ?assertEqual(false, emqx_authn:is_enabled()), emqx_authn:enable(), ?assertEqual(true, emqx_authn:is_enabled()), diff --git a/apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl b/apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl index 7435deaa0..ddb2bb209 100644 --- a/apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl +++ b/apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl @@ -52,21 +52,27 @@ t_jwt_authenticator(_) -> JWS = generate_jws('hmac-based', Payload, <<"abcdef">>), ClientInfo = #{username => <<"myuser">>, 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">>), 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 Config2 = Config#{secret => base64:encode(<<"abcdef">>), secret_base64_encoded => true}, ?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}">>}]}, ?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)), %% Expiration @@ -74,39 +80,39 @@ t_jwt_authenticator(_) -> , <<"exp">> => erlang:system_time(second) - 60}, JWS3 = generate_jws('hmac-based', Payload3, <<"abcdef">>), 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">> , <<"exp">> => erlang:system_time(second) + 60}, JWS4 = generate_jws('hmac-based', Payload4, <<"abcdef">>), ClientInfo4 = ClientInfo#{password => JWS4}, - ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo4, ok)), + ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo4, ignored)), %% Issued At Payload5 = #{ <<"username">> => <<"myuser">> , <<"iat">> => erlang:system_time(second) - 60}, JWS5 = generate_jws('hmac-based', Payload5, <<"abcdef">>), ClientInfo5 = ClientInfo#{password => JWS5}, - ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo5, ok)), + ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo5, ignored)), Payload6 = #{ <<"username">> => <<"myuser">> , <<"iat">> => erlang:system_time(second) + 60}, JWS6 = generate_jws('hmac-based', Payload6, <<"abcdef">>), 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 Payload7 = #{ <<"username">> => <<"myuser">> , <<"nbf">> => erlang:system_time(second) - 60}, JWS7 = generate_jws('hmac-based', Payload7, <<"abcdef">>), ClientInfo7 = ClientInfo#{password => JWS7}, - ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo7, ok)), + ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo7, ignored)), Payload8 = #{ <<"username">> => <<"myuser">> , <<"nbf">> => erlang:system_time(second) + 60}, JWS8 = generate_jws('hmac-based', Payload8, <<"abcdef">>), 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)), ok. @@ -128,8 +134,8 @@ t_jwt_authenticator2(_) -> JWS = generate_jws('public-key', Payload, PrivateKey), ClientInfo = #{username => <<"myuser">>, password => JWS}, - ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo, ok)), - ?assertEqual({stop, {error, not_authorized}}, ?AUTH:authenticate(ClientInfo#{password => <<"badpassword">>}, ok)), + ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)), + ?assertEqual({stop, {error, not_authorized}}, ?AUTH:authenticate(ClientInfo#{password => <<"badpassword">>}, ignored)), ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID)), ok. diff --git a/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl b/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl index fdcaf519d..d6425a89c 100644 --- a/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl +++ b/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl @@ -50,33 +50,36 @@ t_mnesia_authenticator(_) -> UserInfo = #{user_id => <<"myuser">>, password => <<"mypass">>}, - ?assertEqual({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:add_user(?CHAIN, ID, UserInfo)), + ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser">>)), ClientInfo = #{zone => external, username => <<"myuser">>, password => <<"mypass">>}, - ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo, ok)), + ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo, ignored)), ?AUTH:enable(), - ?assertEqual(ok, emqx_access_control:authenticate(ClientInfo)), + ?assertEqual({ok, #{superuser => false}}, emqx_access_control:authenticate(ClientInfo)), 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)), 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)), 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">>}, - ?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({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">>)), ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID)), @@ -104,10 +107,16 @@ t_import(_) -> ClientInfo1 = #{username => <<"myuser1">>, password => <<"mypassword1">>}, - ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo1, ok)), - ClientInfo2 = ClientInfo1#{username => <<"myuser3">>, + ?assertEqual({stop, {ok, #{superuser => true}}}, ?AUTH:authenticate(ClientInfo1, ignored)), + + ClientInfo2 = ClientInfo1#{username => <<"myuser2">>, + password => <<"mypassword2">>}, + ?assertEqual({stop, {ok, #{superuser => false}}}, ?AUTH:authenticate(ClientInfo2, ignored)), + + ClientInfo3 = ClientInfo1#{username => <<"myuser3">>, 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)), ok. @@ -131,11 +140,11 @@ t_multi_mnesia_authenticator(_) -> {ok, #{name := AuthenticatorName1, id := ID1}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig1), {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, #{user_id => <<"myuser">>, password => <<"mypass1">>})), - ?assertEqual({ok, #{user_id => <<"myclient">>}}, + ?assertMatch({ok, #{user_id := <<"myclient">>}}, ?AUTH:add_user(?CHAIN, ID2, #{user_id => <<"myclient">>, password => <<"mypass2">>})), @@ -143,12 +152,12 @@ t_multi_mnesia_authenticator(_) -> ClientInfo1 = #{username => <<"myuser">>, clientid => <<"myclient">>, 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({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">>}, - ?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, ID2)),