% This Source Code Form is subject to the terms of the Mozilla Public
%% License, v. 2.0. If a copy of the MPL was not distributed with this
%% file, You can obtain one at https://mozilla.org/MPL/2.0/.
%%
%% Copyright (c) 2007-2025 Broadcom. All Rights Reserved. The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. All rights reserved.
%%

-module(unit_access_control_SUITE).

-include_lib("common_test/include/ct.hrl").
-include_lib("amqp_client/include/amqp_client.hrl").
-include_lib("eunit/include/eunit.hrl").

-compile([export_all, nowarn_export_all]).

all() ->
    [
      {group, sequential_tests},
      {group, parallel_tests}
    ].

groups() ->
    [
      {parallel_tests, [parallel], [
          password_hashing,
          version_negotiation
      ]},
      {sequential_tests, [], [
          login_with_credentials_but_no_password,
          login_of_passwordless_user,
          set_tags_for_passwordless_user,
          change_password,
          auth_backend_internal_expand_topic_permission,
          rabbit_direct_extract_extra_auth_props
      ]}
    ].

%% -------------------------------------------------------------------
%% Testsuite setup/teardown
%% -------------------------------------------------------------------

init_per_suite(Config) ->
    rabbit_ct_helpers:log_environment(),
    rabbit_ct_helpers:run_setup_steps(Config).

end_per_suite(Config) ->
    rabbit_ct_helpers:run_teardown_steps(Config).

init_per_group(Group, Config) ->
    Config1 = rabbit_ct_helpers:set_config(Config, [
        {rmq_nodename_suffix, Group},
        {rmq_nodes_count, 1}
      ]),
    rabbit_ct_helpers:run_steps(Config1,
      rabbit_ct_broker_helpers:setup_steps() ++
      rabbit_ct_client_helpers:setup_steps()).

end_per_group(_Group, Config) ->
    rabbit_ct_helpers:run_steps(Config,
      rabbit_ct_client_helpers:teardown_steps() ++
      rabbit_ct_broker_helpers:teardown_steps()).

init_per_testcase(Testcase, Config) ->
    rabbit_ct_helpers:testcase_started(Config, Testcase).

end_per_testcase(Testcase, Config) ->
    rabbit_ct_helpers:testcase_finished(Config, Testcase).

%% ---------------------------------------------------------------------------
%% Test Cases
%% ---------------------------------------------------------------------------

password_hashing(Config) ->
    passed = rabbit_ct_broker_helpers:rpc(Config, 0,
      ?MODULE, password_hashing1, [Config]).

password_hashing1(_Config) ->
    rabbit_password_hashing_sha256 = rabbit_password:hashing_mod(),
    application:set_env(rabbit, password_hashing_module,
                        rabbit_password_hashing_md5),
    rabbit_password_hashing_md5    = rabbit_password:hashing_mod(),
    application:set_env(rabbit, password_hashing_module,
                        rabbit_password_hashing_sha256),
    rabbit_password_hashing_sha256 = rabbit_password:hashing_mod(),

    rabbit_password_hashing_sha256 =
        rabbit_password:hashing_mod(rabbit_password_hashing_sha256),
    rabbit_password_hashing_md5    =
        rabbit_password:hashing_mod(rabbit_password_hashing_md5),
    rabbit_password_hashing_md5    =
        rabbit_password:hashing_mod(undefined),

    rabbit_password_hashing_md5    =
        rabbit_auth_backend_internal:hashing_module_for_user(
          internal_user:new()),
    rabbit_password_hashing_md5    =
        rabbit_auth_backend_internal:hashing_module_for_user(
          internal_user:new({hashing_algorithm, undefined})),
    rabbit_password_hashing_md5    =
        rabbit_auth_backend_internal:hashing_module_for_user(
          internal_user:new({hashing_algorithm, rabbit_password_hashing_md5})),

    rabbit_password_hashing_sha256 =
        rabbit_auth_backend_internal:hashing_module_for_user(
          internal_user:new({hashing_algorithm, rabbit_password_hashing_sha256})),

    passed.

change_password(Config) ->
    passed = rabbit_ct_broker_helpers:rpc(Config, 0,
      ?MODULE, change_password1, [Config]).

change_password1(_Config) ->
    UserName = <<"test_user">>,
    Password = <<"test_password">>,
    case rabbit_auth_backend_internal:lookup_user(UserName) of
        {ok, _} -> rabbit_auth_backend_internal:delete_user(UserName, <<"acting-user">>);
        _       -> ok
    end,
    ok = application:set_env(rabbit, password_hashing_module,
                             rabbit_password_hashing_md5),
    ok = rabbit_auth_backend_internal:add_user(UserName, Password, <<"acting-user">>),
    {ok, #auth_user{username = UserName}} =
        rabbit_auth_backend_internal:user_login_authentication(
            UserName, [{password, Password}]),
    ok = application:set_env(rabbit, password_hashing_module,
                             rabbit_password_hashing_sha256),
    {ok, #auth_user{username = UserName}} =
        rabbit_auth_backend_internal:user_login_authentication(
            UserName, [{password, Password}]),

    NewPassword = <<"test_password1">>,
    ok = rabbit_auth_backend_internal:change_password(UserName, NewPassword,
                                                      <<"acting-user">>),
    {ok, #auth_user{username = UserName}} =
        rabbit_auth_backend_internal:user_login_authentication(
            UserName, [{password, NewPassword}]),

    {refused, _, [UserName]} =
        rabbit_auth_backend_internal:user_login_authentication(
            UserName, [{password, Password}]),
    passed.


login_with_credentials_but_no_password(Config) ->
    passed = rabbit_ct_broker_helpers:rpc(Config, 0,
      ?MODULE, login_with_credentials_but_no_password1, [Config]).

login_with_credentials_but_no_password1(_Config) ->
    Username = <<"login_with_credentials_but_no_password-user">>,
    Password = <<"login_with_credentials_but_no_password-password">>,
    ok = rabbit_auth_backend_internal:add_user(Username, Password, <<"acting-user">>),

    ?assertMatch(
       {refused, _Message, [Username]},
        rabbit_auth_backend_internal:user_login_authentication(Username,
                                                              [{key, <<"value">>}])),

    ok = rabbit_auth_backend_internal:delete_user(Username, <<"acting-user">>),

    passed.

%% passwordless users are not supposed to be used with
%% this backend (and PLAIN authentication mechanism in general)
login_of_passwordless_user(Config) ->
    passed = rabbit_ct_broker_helpers:rpc(Config, 0,
      ?MODULE, login_of_passwordless_user1, [Config]).

login_of_passwordless_user1(_Config) ->
    Username = <<"login_of_passwordless_user-user">>,
    Password = <<"">>,
    ok = rabbit_auth_backend_internal:add_user(Username, Password, <<"acting-user">>),

    ?assertMatch(
       {refused, _Message, [Username]},
       rabbit_auth_backend_internal:user_login_authentication(Username,
                                                              [{password, <<"">>}])),

    ?assertMatch(
       {refused, _Format, [Username]},
       rabbit_auth_backend_internal:user_login_authentication(Username,
                                                              [{password, ""}])),

    ok = rabbit_auth_backend_internal:delete_user(Username, <<"acting-user">>),

    passed.


set_tags_for_passwordless_user(Config) ->
    passed = rabbit_ct_broker_helpers:rpc(Config, 0,
      ?MODULE, set_tags_for_passwordless_user1, [Config]).

set_tags_for_passwordless_user1(_Config) ->
    Username = <<"set_tags_for_passwordless_user">>,
    Password = <<"set_tags_for_passwordless_user">>,
    ok = rabbit_auth_backend_internal:add_user(Username, Password,
                                               <<"acting-user">>),
    ok = rabbit_auth_backend_internal:clear_password(Username,
                                                     <<"acting-user">>),
    ok = rabbit_auth_backend_internal:set_tags(Username, [management],
                                               <<"acting-user">>),

    {ok, User1} = rabbit_auth_backend_internal:lookup_user(Username),
    ?assertEqual([management], internal_user:get_tags(User1)),

    ok = rabbit_auth_backend_internal:set_tags(Username, [management, policymaker],
                                               <<"acting-user">>),

    {ok, User2} = rabbit_auth_backend_internal:lookup_user(Username),
    ?assertEqual([management, policymaker], internal_user:get_tags(User2)),

    ok = rabbit_auth_backend_internal:set_tags(Username, [],
                                               <<"acting-user">>),

    {ok, User3} = rabbit_auth_backend_internal:lookup_user(Username),
    ?assertEqual([], internal_user:get_tags(User3)),

    ok = rabbit_auth_backend_internal:delete_user(Username,
                                                  <<"acting-user">>),

    passed.


rabbit_direct_extract_extra_auth_props(_Config) ->
    {ok, CSC} = code_server_cache:start_link(),
    % no protocol to extract
    [] = rabbit_direct:extract_extra_auth_props(
        {<<"guest">>, <<"guest">>}, <<"/">>, 1,
        [{name,<<"127.0.0.1:52366 -> 127.0.0.1:1883">>}]),
    % protocol to extract, but no module to call
    [] = rabbit_direct:extract_extra_auth_props(
        {<<"guest">>, <<"guest">>}, <<"/">>, 1,
        [{protocol, {'PROTOCOL_WITHOUT_MODULE', "1.0"}}]),
    % see rabbit_dummy_protocol_connection_info module
    % protocol to extract, module that returns a client ID
    [{client_id, <<"DummyClientId">>}] = rabbit_direct:extract_extra_auth_props(
        {<<"guest">>, <<"guest">>}, <<"/">>, 1,
        [{protocol, {'DUMMY_PROTOCOL', "1.0"}}]),
    % protocol to extract, but error thrown in module
    [] = rabbit_direct:extract_extra_auth_props(
        {<<"guest">>, <<"guest">>}, <<"/">>, -1,
        [{protocol, {'DUMMY_PROTOCOL', "1.0"}}]),
    gen_server:stop(CSC),
    ok.

auth_backend_internal_expand_topic_permission(_Config) ->
    ExpandMap = #{<<"username">> => <<"guest">>, <<"vhost">> => <<"default">>},
    %% simple case
    <<"services/default/accounts/guest/notifications">> =
        rabbit_auth_backend_internal:expand_topic_permission(
            <<"services/{vhost}/accounts/{username}/notifications">>,
            ExpandMap
        ),
    %% replace variable twice
    <<"services/default/accounts/default/guest/notifications">> =
        rabbit_auth_backend_internal:expand_topic_permission(
            <<"services/{vhost}/accounts/{vhost}/{username}/notifications">>,
            ExpandMap
        ),
    %% nothing to replace
    <<"services/accounts/notifications">> =
        rabbit_auth_backend_internal:expand_topic_permission(
            <<"services/accounts/notifications">>,
            ExpandMap
        ),
    %% the expand map isn't defined
    <<"services/{vhost}/accounts/{username}/notifications">> =
        rabbit_auth_backend_internal:expand_topic_permission(
            <<"services/{vhost}/accounts/{username}/notifications">>,
            undefined
        ),
    %% the expand map is empty
    <<"services/{vhost}/accounts/{username}/notifications">> =
        rabbit_auth_backend_internal:expand_topic_permission(
            <<"services/{vhost}/accounts/{username}/notifications">>,
            #{}
        ),
    ok.

%% Test AMQP 1.0 §2.2
version_negotiation(Config) ->
    ok = rabbit_ct_broker_helpers:rpc(Config, ?MODULE, version_negotiation1, [Config]).

version_negotiation1(Config) ->
    Hostname = ?config(rmq_hostname, Config),
    Port = rabbit_ct_broker_helpers:get_node_config(Config, 0, tcp_port_amqp),

    [?assertEqual(<<"AMQP",3,1,0,0>>,
                  version_negotiation2(Hostname, Port, Vsn)) ||
     Vsn <- [<<"AMQP",0,1,0,0>>,
             <<"AMQP",0,1,0,1>>,
             <<"AMQP",0,1,1,0>>,
             <<"AMQP",0,9,1,0>>,
             <<"AMQP",0,0,8,0>>,
             <<"AMQP",1,1,0,0>>,
             <<"AMQP",2,1,0,0>>,
             <<"AMQP",3,1,0,0>>,
             <<"AMQP",3,1,0,1>>,
             <<"AMQP",3,1,0,1>>,
             <<"AMQP",4,1,0,0>>,
             <<"AMQP",9,1,0,0>>,
             <<"XXXX",0,1,0,0>>,
             <<"XXXX",0,0,9,1>>
            ]],

    [?assertEqual(<<"AMQP",0,0,9,1>>,
                  version_negotiation2(Hostname, Port, Vsn)) ||
     Vsn <- [<<"AMQP",0,0,9,2>>,
             <<"AMQP",0,0,10,0>>,
             <<"AMQP",0,0,10,1>>]],
    ok.

version_negotiation2(Hostname, Port, Header) ->
    {ok, C} = gen_tcp:connect(Hostname, Port, [binary, {active, false}]),
    ok = gen_tcp:send(C, Header),
    {ok, ServerVersion} = gen_tcp:recv(C, 8, 100),
    ok = gen_tcp:close(C),
    ServerVersion.
