Merge pull request #13387 from zhongwencool/dont-override-authn-users

fix: don't override authn users when import_user from authn.boostrap_file
This commit is contained in:
zhongwencool 2024-07-02 22:33:16 +08:00 committed by GitHub
commit 55298ab6f3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 91 additions and 82 deletions

View File

@ -0,0 +1 @@
user_id,password,is_superuser
1 user_id password is_superuser

View File

@ -0,0 +1,3 @@
user_id,password,is_superuser
myuser3,Password4,true
myuser4,Password3,false
1 user_id password is_superuser
2 myuser3 Password4 true
3 myuser4 Password3 false

View File

@ -171,67 +171,61 @@ do_destroy(UserGroup) ->
mnesia:select(?TAB, group_match_spec(UserGroup), write) mnesia:select(?TAB, group_match_spec(UserGroup), write)
). ).
import_users({PasswordType, Filename, FileData}, State) -> import_users(ImportSource, State) ->
import_users(ImportSource, State, #{override => true}).
import_users({PasswordType, Filename, FileData}, State, Opts) ->
Convertor = convertor(PasswordType, State), Convertor = convertor(PasswordType, State),
try try parse_import_users(Filename, FileData, Convertor) of
{_NewUsersCnt, Users} = parse_import_users(Filename, FileData, Convertor), Users ->
case length(Users) > 0 andalso do_import_users(Users) of case do_import_users(Users, Opts#{filename => Filename}) of
false -> ok ->
error(empty_users); ok;
ok -> %% Do not log empty user entries.
ok; %% The default etc/auth-built-in-db.csv file contains an empty user entry.
{error, Reason} -> {error, empty_users} ->
_ = do_clean_imported_users(Users), {error, empty_users};
error(Reason) {error, Reason} ->
end ?SLOG(
warning,
#{
msg => "import_authn_users_failed",
reason => Reason,
type => PasswordType,
filename => Filename
}
),
{error, Reason}
end
catch catch
error:Reason1:Stk -> error:Reason:Stk ->
?SLOG( ?SLOG(
warning, warning,
#{ #{
msg => "import_users_failed", msg => "parse_authn_users_failed",
reason => Reason1, reason => Reason,
type => PasswordType, type => PasswordType,
filename => Filename, filename => Filename,
stacktrace => Stk stacktrace => Stk
} }
), ),
{error, Reason1} {error, Reason}
end. end.
do_import_users(Users) -> do_import_users([], _Opts) ->
{error, empty_users};
do_import_users(Users, Opts) ->
trans( trans(
fun() -> fun() ->
lists:foreach( lists:foreach(
fun( fun(User) ->
#{ insert_user(User, Opts)
<<"user_group">> := UserGroup,
<<"user_id">> := UserID,
<<"password_hash">> := PasswordHash,
<<"salt">> := Salt,
<<"is_superuser">> := IsSuperuser
}
) ->
insert_user(UserGroup, UserID, PasswordHash, Salt, IsSuperuser)
end, end,
Users Users
) )
end end
). ).
do_clean_imported_users(Users) ->
lists:foreach(
fun(
#{
<<"user_group">> := UserGroup,
<<"user_id">> := UserID
}
) ->
mria:dirty_delete(?TAB, {UserGroup, UserID})
end,
Users
).
add_user( add_user(
UserInfo, UserInfo,
State State
@ -338,7 +332,14 @@ run_fuzzy_filter(
%% Internal functions %% Internal functions
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
insert_user(UserGroup, UserID, PasswordHash, Salt, IsSuperuser) -> insert_user(User, Opts) ->
#{
<<"user_group">> := UserGroup,
<<"user_id">> := UserID,
<<"password_hash">> := PasswordHash,
<<"salt">> := Salt,
<<"is_superuser">> := IsSuperuser
} = User,
UserInfoRecord = UserInfoRecord =
#user_info{user_id = DBUserID} = #user_info{user_id = DBUserID} =
user_info_record(UserGroup, UserID, PasswordHash, Salt, IsSuperuser), user_info_record(UserGroup, UserID, PasswordHash, Salt, IsSuperuser),
@ -348,14 +349,22 @@ insert_user(UserGroup, UserID, PasswordHash, Salt, IsSuperuser) ->
[UserInfoRecord] -> [UserInfoRecord] ->
ok; ok;
[_] -> [_] ->
Msg =
case maps:get(override, Opts, false) of
true ->
insert_user(UserInfoRecord),
"override_an_exists_userid_into_authentication_database_ok";
false ->
"import_an_exists_userid_into_authentication_database_failed"
end,
?SLOG(warning, #{ ?SLOG(warning, #{
msg => "bootstrap_authentication_overridden_in_the_built_in_database", msg => Msg,
user_id => UserID, user_id => UserID,
group_id => UserGroup, group_id => UserGroup,
bootstrap_file => maps:get(filename, Opts),
suggestion => suggestion =>
"If you have made changes in other way, remove the user_id from the bootstrap file." "If you've altered it differently, delete the user_id from the bootstrap file."
}), })
insert_user(UserInfoRecord)
end. end.
insert_user(#user_info{} = UserInfoRecord) -> insert_user(#user_info{} = UserInfoRecord) ->
@ -473,27 +482,7 @@ parse_import_users(Filename, FileData, Convertor) ->
end end
end, end,
ReaderFn = reader_fn(Filename, FileData), ReaderFn = reader_fn(Filename, FileData),
Users = Eval(ReaderFn), Eval(ReaderFn).
NewUsersCount =
lists:foldl(
fun(
#{
<<"user_group">> := UserGroup,
<<"user_id">> := UserID
},
Acc
) ->
case ets:member(?TAB, {UserGroup, UserID}) of
true ->
Acc;
false ->
Acc + 1
end
end,
0,
Users
),
{NewUsersCount, Users}.
reader_fn(prepared_user_list, List) when is_list(List) -> reader_fn(prepared_user_list, List) when is_list(List) ->
%% Example: [#{<<"user_id">> => <<>>, ...}] %% Example: [#{<<"user_id">> => <<>>, ...}]
@ -511,7 +500,7 @@ reader_fn(Filename0, Data) ->
error(Reason) error(Reason)
end; end;
<<".csv">> -> <<".csv">> ->
%% Example: data/user-credentials.csv %% Example: etc/auth-built-in-db-bootstrap.csv
emqx_utils_stream:csv(Data); emqx_utils_stream:csv(Data);
<<>> -> <<>> ->
error(unknown_file_format); error(unknown_file_format);
@ -556,16 +545,15 @@ is_superuser(#{<<"is_superuser">> := true}) -> true;
is_superuser(_) -> false. is_superuser(_) -> false.
boostrap_user_from_file(Config, State) -> boostrap_user_from_file(Config, State) ->
case maps:get(boostrap_file, Config, <<>>) of case maps:get(bootstrap_file, Config, <<>>) of
<<>> -> <<>> ->
ok; ok;
FileName0 -> FileName0 ->
#{boostrap_type := Type} = Config, #{bootstrap_type := Type} = Config,
FileName = emqx_schema:naive_env_interpolation(FileName0), FileName = emqx_schema:naive_env_interpolation(FileName0),
case file:read_file(FileName) of case file:read_file(FileName) of
{ok, FileData} -> {ok, FileData} ->
%% if there is a key conflict, override with the key which from the bootstrap file _ = import_users({Type, FileName, FileData}, State, #{override => false}),
_ = import_users({Type, FileName, FileData}, State),
ok; ok;
{error, Reason} -> {error, Reason} ->
?SLOG(warning, #{ ?SLOG(warning, #{

View File

@ -46,7 +46,7 @@ select_union_member(_Kind, _Value) ->
fields(builtin_db) -> fields(builtin_db) ->
[ [
{password_hash_algorithm, fun emqx_authn_password_hashing:type_rw/1} {password_hash_algorithm, fun emqx_authn_password_hashing:type_rw/1}
] ++ common_fields() ++ bootstrap_fields(); ] ++ common_fields();
fields(builtin_db_api) -> fields(builtin_db_api) ->
[ [
{password_hash_algorithm, fun emqx_authn_password_hashing:type_rw_api/1} {password_hash_algorithm, fun emqx_authn_password_hashing:type_rw_api/1}
@ -68,7 +68,8 @@ common_fields() ->
{mechanism, emqx_authn_schema:mechanism(?AUTHN_MECHANISM_SIMPLE)}, {mechanism, emqx_authn_schema:mechanism(?AUTHN_MECHANISM_SIMPLE)},
{backend, emqx_authn_schema:backend(?AUTHN_BACKEND)}, {backend, emqx_authn_schema:backend(?AUTHN_BACKEND)},
{user_id_type, fun user_id_type/1} {user_id_type, fun user_id_type/1}
] ++ emqx_authn_schema:common_fields(). ] ++ bootstrap_fields() ++
emqx_authn_schema:common_fields().
bootstrap_fields() -> bootstrap_fields() ->
[ [
@ -78,7 +79,7 @@ bootstrap_fields() ->
#{ #{
desc => ?DESC(bootstrap_file), desc => ?DESC(bootstrap_file),
required => false, required => false,
default => <<>> default => <<"${EMQX_ETC_DIR}/auth-built-in-db-bootstrap.csv">>
} }
)}, )},
{bootstrap_type, {bootstrap_type,

View File

@ -56,6 +56,7 @@ t_create(_) ->
Config1 = Config0#{password_hash_algorithm => #{name => sha256}}, Config1 = Config0#{password_hash_algorithm => #{name => sha256}},
{ok, _} = emqx_authn_mnesia:create(?AUTHN_ID, Config1), {ok, _} = emqx_authn_mnesia:create(?AUTHN_ID, Config1),
ok. ok.
t_bootstrap_file(_) -> t_bootstrap_file(_) ->
Config = config(), Config = config(),
%% hash to hash %% hash to hash
@ -102,25 +103,39 @@ t_bootstrap_file(_) ->
], ],
test_bootstrap_file(HashConfig, plain, <<"user-credentials-plain.json">>) test_bootstrap_file(HashConfig, plain, <<"user-credentials-plain.json">>)
), ),
Opts = #{clean => false},
Result = test_bootstrap_file(HashConfig, plain, <<"user-credentials-plain.csv">>, Opts),
?assertMatch( ?assertMatch(
[ [
{user_info, {_, <<"myuser3">>}, _, _, true}, {user_info, {_, <<"myuser3">>}, _, _, true},
{user_info, {_, <<"myuser4">>}, _, _, false} {user_info, {_, <<"myuser4">>}, _, _, false}
], ],
test_bootstrap_file(HashConfig, plain, <<"user-credentials-plain.csv">>) Result
),
%% Don't override the exist user id.
?assertMatch(
Result, test_bootstrap_file(HashConfig, plain, <<"user-credentials-plain_v2.csv">>)
), ),
ok. ok.
test_bootstrap_file(Config0, Type, File) -> test_bootstrap_file(Config0, Type, File) ->
test_bootstrap_file(Config0, Type, File, #{clean => true}).
test_bootstrap_file(Config0, Type, File, Opts) ->
{Type, Filename, _FileData} = sample_filename_and_data(Type, File), {Type, Filename, _FileData} = sample_filename_and_data(Type, File),
Config2 = Config0#{ Config2 = Config0#{
boostrap_file => Filename, bootstrap_file => Filename,
boostrap_type => Type bootstrap_type => Type
}, },
{ok, State0} = emqx_authn_mnesia:create(?AUTHN_ID, Config2), {ok, State0} = emqx_authn_mnesia:create(?AUTHN_ID, Config2),
Result = ets:tab2list(emqx_authn_mnesia), Result = ets:tab2list(emqx_authn_mnesia),
ok = emqx_authn_mnesia:destroy(State0), case maps:get(clean, Opts) of
?assertMatch([], ets:tab2list(emqx_authn_mnesia)), true ->
ok = emqx_authn_mnesia:destroy(State0),
?assertMatch([], ets:tab2list(emqx_authn_mnesia));
_ ->
ok
end,
Result. Result.
t_update(_) -> t_update(_) ->

View File

@ -469,6 +469,8 @@ relx_overlay(ReleaseType, Edition) ->
{copy, "bin/install_upgrade.escript", "bin/install_upgrade.escript-{{release_version}}"}, {copy, "bin/install_upgrade.escript", "bin/install_upgrade.escript-{{release_version}}"},
{copy, "apps/emqx_gateway_lwm2m/lwm2m_xml", "etc/lwm2m_xml"}, {copy, "apps/emqx_gateway_lwm2m/lwm2m_xml", "etc/lwm2m_xml"},
{copy, "apps/emqx_auth/etc/acl.conf", "etc/acl.conf"}, {copy, "apps/emqx_auth/etc/acl.conf", "etc/acl.conf"},
{copy, "apps/emqx_auth/etc/auth-built-in-db-bootstrap.csv",
"etc/auth-built-in-db-bootstrap.csv"},
{template, "bin/emqx.cmd", "bin/emqx.cmd"}, {template, "bin/emqx.cmd", "bin/emqx.cmd"},
{template, "bin/emqx_ctl.cmd", "bin/emqx_ctl.cmd"}, {template, "bin/emqx_ctl.cmd", "bin/emqx_ctl.cmd"},
{copy, "bin/nodetool", "bin/nodetool"}, {copy, "bin/nodetool", "bin/nodetool"},

View File

@ -11,9 +11,8 @@ user_id_type.label:
bootstrap_file.desc: bootstrap_file.desc:
"""The bootstrap file imports users into the built-in database. """The bootstrap file imports users into the built-in database.
The file content format is determined by `bootstrap_type`. It will not import a user ID that already exists in the database.
Remove the item from the bootstrap file when you have made changes in other way, The file content format is determined by `bootstrap_type`."""
otherwise, after restarting, the bootstrap item will be overridden again."""
bootstrap_file.label: bootstrap_file.label:
"""Bootstrap File Path""" """Bootstrap File Path"""