947 lines
41 KiB
Erlang
947 lines
41 KiB
Erlang
%% 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(system_SUITE).
|
|
-compile([export_all]).
|
|
|
|
-include_lib("common_test/include/ct.hrl").
|
|
-include_lib("eunit/include/eunit.hrl").
|
|
-include_lib("amqp_client/include/amqp_client.hrl").
|
|
|
|
-define(ALICE_NAME, "Alice").
|
|
-define(BOB_NAME, "Bob").
|
|
-define(CAROL_NAME, "Carol").
|
|
-define(PETER_NAME, "Peter").
|
|
-define(JIMMY_NAME, "Jimmy").
|
|
|
|
-define(VHOST, "test").
|
|
|
|
-define(ALICE, #amqp_params_network{username = <<?ALICE_NAME>>,
|
|
password = <<"password">>,
|
|
virtual_host = <<?VHOST>>}).
|
|
|
|
-define(BOB, #amqp_params_network{username = <<?BOB_NAME>>,
|
|
password = <<"password">>,
|
|
virtual_host = <<?VHOST>>}).
|
|
|
|
-define(CAROL, #amqp_params_network{username = <<?CAROL_NAME>>,
|
|
password = <<"password">>,
|
|
virtual_host = <<?VHOST>>}).
|
|
|
|
-define(PETER, #amqp_params_network{username = <<?PETER_NAME>>,
|
|
password = <<"password">>,
|
|
virtual_host = <<?VHOST>>}).
|
|
|
|
-define(JIMMY, #amqp_params_network{username = <<?JIMMY_NAME>>,
|
|
password = <<"password">>,
|
|
virtual_host = <<?VHOST>>}).
|
|
|
|
-define(BASE_CONF_RABBIT, {rabbit, [{default_vhost, <<"test">>}]}).
|
|
|
|
base_conf_ldap(LdapPort, IdleTimeout, PoolSize) ->
|
|
{rabbitmq_auth_backend_ldap, [{servers, ["localhost"]},
|
|
{user_dn_pattern, "cn=${username},ou=People,dc=rabbitmq,dc=com"},
|
|
{other_bind, anon},
|
|
{use_ssl, false},
|
|
{port, LdapPort},
|
|
{idle_timeout, IdleTimeout},
|
|
{pool_size, PoolSize},
|
|
{log, true},
|
|
{group_lookup_base, "ou=groups,dc=rabbitmq,dc=com"},
|
|
{vhost_access_query, vhost_access_query_base()},
|
|
{resource_access_query,
|
|
{for, [{resource, exchange,
|
|
{for, [{permission, configure,
|
|
{in_group, "cn=wheel,ou=groups,dc=rabbitmq,dc=com"}
|
|
},
|
|
{permission, write, {constant, true}},
|
|
{permission, read,
|
|
{match, {string, "${name}"},
|
|
{string, "^xch-${username}-.*"}}
|
|
}
|
|
]}},
|
|
{resource, queue,
|
|
{for, [{permission, configure,
|
|
{match, {attribute, "${user_dn}", "description"},
|
|
{string, "can-declare-queues"}}
|
|
},
|
|
{permission, write, {constant, true}},
|
|
{permission, read,
|
|
{'or',
|
|
[{'and',
|
|
[{equals, "${name}", "test1"},
|
|
{equals, "${username}", "Alice"}]},
|
|
{'and',
|
|
[{equals, "${name}", "test2"},
|
|
{'not', {equals, "${username}", "Bob"}}]}
|
|
]}}
|
|
]}}
|
|
]}},
|
|
{topic_access_query, topic_access_query_base()},
|
|
{tag_queries, [{monitor, {constant, true}},
|
|
{administrator, {constant, false}},
|
|
{management, {constant, false}}]}
|
|
]}.
|
|
|
|
%%--------------------------------------------------------------------
|
|
|
|
all() ->
|
|
[
|
|
{group, non_parallel_tests},
|
|
{group, with_idle_timeout}
|
|
].
|
|
|
|
groups() ->
|
|
Tests = [
|
|
purge_connection,
|
|
ldap_only,
|
|
ldap_and_internal,
|
|
internal_followed_ldap_and_internal,
|
|
tag_attribution_ldap_only,
|
|
tag_attribution_ldap_and_internal,
|
|
tag_attribution_internal_followed_by_ldap_and_internal,
|
|
invalid_or_clause_ldap_only,
|
|
invalid_and_clause_ldap_only,
|
|
topic_authorisation_publishing_ldap_only,
|
|
topic_authorisation_consumption,
|
|
match_bidirectional,
|
|
match_bidirectional_gh_100
|
|
],
|
|
[
|
|
{non_parallel_tests, [], Tests
|
|
},
|
|
{with_idle_timeout, [], [connections_closed_after_timeout | Tests]
|
|
}
|
|
].
|
|
|
|
suite() ->
|
|
[{timetrap, {minutes, 2}}].
|
|
|
|
%% -------------------------------------------------------------------
|
|
%% Testsuite setup/teardown.
|
|
%% -------------------------------------------------------------------
|
|
|
|
init_per_suite(Config) ->
|
|
rabbit_ct_helpers:log_environment(),
|
|
rabbit_ct_helpers:run_setup_steps(Config, [fun init_slapd/1]).
|
|
|
|
end_per_suite(Config) ->
|
|
rabbit_ct_helpers:run_teardown_steps(Config, [fun stop_slapd/1]).
|
|
|
|
init_per_group(Group, Config) ->
|
|
Config1 = rabbit_ct_helpers:set_config(Config, [
|
|
{rmq_nodename_suffix, Group}
|
|
]),
|
|
LdapPort = ?config(ldap_port, Config),
|
|
Config2 = rabbit_ct_helpers:merge_app_env(Config1, ?BASE_CONF_RABBIT),
|
|
Config3 = rabbit_ct_helpers:merge_app_env(Config2,
|
|
base_conf_ldap(LdapPort,
|
|
idle_timeout(Group),
|
|
pool_size(Group))),
|
|
rabbit_ldap_seed:seed({"localhost", LdapPort}),
|
|
Config4 = rabbit_ct_helpers:set_config(Config3, {ldap_port, LdapPort}),
|
|
|
|
rabbit_ct_helpers:run_steps(Config4,
|
|
rabbit_ct_broker_helpers:setup_steps() ++
|
|
rabbit_ct_client_helpers:setup_steps()).
|
|
|
|
end_per_group(_, Config) ->
|
|
rabbit_ldap_seed:delete({"localhost", ?config(ldap_port, Config)}),
|
|
rabbit_ct_helpers:run_steps(Config,
|
|
rabbit_ct_client_helpers:teardown_steps() ++
|
|
rabbit_ct_broker_helpers:teardown_steps()).
|
|
|
|
init_slapd(Config) ->
|
|
DataDir = ?config(data_dir, Config),
|
|
PrivDir = ?config(priv_dir, Config),
|
|
TcpPort = 25389,
|
|
SlapdDir = filename:join([PrivDir, "openldap"]),
|
|
InitSlapd = filename:join([DataDir, "init-slapd.sh"]),
|
|
Cmd = [InitSlapd, SlapdDir, {"~b", [TcpPort]}],
|
|
case rabbit_ct_helpers:exec(Cmd) of
|
|
{ok, Stdout} ->
|
|
{match, [SlapdPid]} = re:run(
|
|
Stdout,
|
|
"^SLAPD_PID=([0-9]+)$",
|
|
[{capture, all_but_first, list},
|
|
multiline]),
|
|
ct:pal(?LOW_IMPORTANCE,
|
|
"slapd(8) PID: ~ts~nslapd(8) listening on: ~b",
|
|
[SlapdPid, TcpPort]),
|
|
rabbit_ct_helpers:set_config(Config,
|
|
[{slapd_pid, SlapdPid},
|
|
{ldap_port, TcpPort}]);
|
|
_ ->
|
|
_ = rabbit_ct_helpers:exec(["pkill", "-INT", "slapd"]),
|
|
{skip, "Failed to initialize slapd(8)"}
|
|
end.
|
|
|
|
stop_slapd(Config) ->
|
|
SlapdPid = ?config(slapd_pid, Config),
|
|
Cmd = ["kill", "-INT", SlapdPid],
|
|
_ = rabbit_ct_helpers:exec(Cmd),
|
|
Config.
|
|
|
|
idle_timeout(with_idle_timeout) -> 2000;
|
|
idle_timeout(non_parallel_tests) -> infinity.
|
|
|
|
pool_size(with_idle_timeout) -> 1;
|
|
pool_size(non_parallel_tests) -> 10.
|
|
|
|
init_internal(Config) ->
|
|
ok = control_action(Config, add_user, [?ALICE_NAME, ""]),
|
|
ok = control_action(Config, set_permissions, [?ALICE_NAME, "prefix-.*", "prefix-.*", "prefix-.*"]),
|
|
ok = control_action(Config, set_user_tags, [?ALICE_NAME, "management", "foo"]),
|
|
ok = control_action(Config, add_user, [?BOB_NAME, ""]),
|
|
ok = control_action(Config, set_permissions, [?BOB_NAME, "^$", "^$", "^$"]),
|
|
ok = control_action(Config, add_user, [?PETER_NAME, ""]),
|
|
ok = control_action(Config, set_permissions, [?PETER_NAME, "^$", "^$", "^$"]).
|
|
|
|
end_internal(Config) ->
|
|
ok = control_action(Config, delete_user, [?ALICE_NAME]),
|
|
ok = control_action(Config, delete_user, [?BOB_NAME]),
|
|
ok = control_action(Config, delete_user, [?PETER_NAME]).
|
|
|
|
init_per_testcase(Testcase, Config)
|
|
when Testcase == ldap_and_internal;
|
|
Testcase == internal_followed_ldap_and_internal ->
|
|
init_internal(Config),
|
|
rabbit_ct_helpers:testcase_started(Config, Testcase);
|
|
init_per_testcase(Testcase, Config)
|
|
when Testcase == tag_attribution_ldap_and_internal;
|
|
Testcase == tag_attribution_internal_followed_by_ldap_and_internal ->
|
|
% back up tag queries
|
|
Cfg = case rabbit_ct_broker_helpers:rpc(Config, 0,
|
|
application,
|
|
get_env,
|
|
[rabbit_auth_backend_ldap, tag_queries]) of
|
|
undefined -> undefined;
|
|
{ok, X} -> X
|
|
end,
|
|
rabbit_ct_helpers:set_config(Config, {tag_queries_config, Cfg}),
|
|
internal_authorization_teardown(Config),
|
|
internal_authorization_setup(Config),
|
|
rabbit_ct_helpers:testcase_started(Config, Testcase);
|
|
init_per_testcase(Testcase, Config) ->
|
|
rabbit_ct_helpers:testcase_started(Config, Testcase).
|
|
|
|
end_per_testcase(Testcase, Config)
|
|
when Testcase == ldap_and_internal;
|
|
Testcase == internal_followed_ldap_and_internal ->
|
|
end_internal(Config),
|
|
rabbit_ct_helpers:testcase_finished(Config, Testcase);
|
|
end_per_testcase(Testcase, Config)
|
|
when Testcase == tag_attribution_ldap_and_internal;
|
|
Testcase == tag_attribution_internal_followed_by_ldap_and_internal ->
|
|
% restore tag queries
|
|
Cfg = rabbit_ct_helpers:get_config(Config, tag_queries_config),
|
|
ok = rabbit_ct_broker_helpers:rpc(Config, 0,
|
|
application,
|
|
set_env,
|
|
[rabbit_auth_backend_ldap, tag_queries, Cfg]),
|
|
internal_authorization_teardown(Config),
|
|
rabbit_ct_helpers:testcase_finished(Config, Testcase);
|
|
end_per_testcase(connections_closed_after_timeout, Config) ->
|
|
ok = rabbit_ct_broker_helpers:rpc(Config, 0,
|
|
application,
|
|
set_env,
|
|
[rabbitmq_auth_backend_ldap,
|
|
other_bind, anon]),
|
|
rabbit_ct_helpers:testcase_finished(Config, connections_closed_after_timeout);
|
|
end_per_testcase(Testcase, Config)
|
|
when Testcase == invalid_or_clause_ldap_only;
|
|
Testcase == invalid_and_clause_ldap_only ->
|
|
set_env(Config, vhost_access_query_base_env()),
|
|
rabbit_ct_helpers:testcase_finished(Config, Testcase);
|
|
end_per_testcase(Testcase, Config)
|
|
when Testcase == topic_authorisation_publishing_ldap_only;
|
|
Testcase == topic_authorisation_consumption ->
|
|
set_env(Config, topic_access_query_base_env()),
|
|
rabbit_ct_helpers:testcase_finished(Config, Testcase);
|
|
end_per_testcase(Testcase, Config) ->
|
|
rabbit_ct_helpers:testcase_finished(Config, Testcase).
|
|
|
|
|
|
%% -------------------------------------------------------------------
|
|
%% Testsuite cases
|
|
%% -------------------------------------------------------------------
|
|
|
|
purge_connection(Config) ->
|
|
{ok, _} = rabbit_ct_broker_helpers:rpc(Config, 0,
|
|
rabbit_auth_backend_ldap,
|
|
user_login_authentication,
|
|
[<<?ALICE_NAME>>, []]),
|
|
|
|
ok = rabbit_ct_broker_helpers:rpc(Config, 0,
|
|
rabbit_auth_backend_ldap,
|
|
purge_connections, []).
|
|
|
|
connections_closed_after_timeout(Config) ->
|
|
{ok, _} = rabbit_ct_broker_helpers:rpc(Config, 0,
|
|
rabbit_auth_backend_ldap,
|
|
user_login_authentication,
|
|
[<<?ALICE_NAME>>, []]),
|
|
|
|
[_] = maps:to_list(get_ldap_connections(Config)),
|
|
ct:sleep(idle_timeout(with_idle_timeout) + 200),
|
|
|
|
%% There should be no connections after idle timeout
|
|
[] = maps:to_list(get_ldap_connections(Config)),
|
|
|
|
{ok, _} = rabbit_ct_broker_helpers:rpc(Config, 0,
|
|
rabbit_auth_backend_ldap,
|
|
user_login_authentication,
|
|
[<<?ALICE_NAME>>, []]),
|
|
|
|
ct:sleep(round(idle_timeout(with_idle_timeout)/2)),
|
|
|
|
%% Login with password opens different connection,
|
|
% so unauthorized connection will be closed
|
|
ok = rabbit_ct_broker_helpers:rpc(Config, 0,
|
|
application,
|
|
set_env,
|
|
[rabbitmq_auth_backend_ldap,
|
|
other_bind, as_user]),
|
|
{ok, _} = rabbit_ct_broker_helpers:rpc(Config, 0,
|
|
rabbit_auth_backend_ldap,
|
|
user_login_authentication,
|
|
[<<?ALICE_NAME>>,
|
|
[{password, <<"password">>}]]),
|
|
|
|
ct:sleep(round(idle_timeout(with_idle_timeout)/2)),
|
|
|
|
{ok, _} = rabbit_ct_broker_helpers:rpc(Config, 0,
|
|
rabbit_auth_backend_ldap,
|
|
user_login_authentication,
|
|
[<<?ALICE_NAME>>,
|
|
[{password, <<"password">>}]]),
|
|
|
|
ct:sleep(round(idle_timeout(with_idle_timeout)/2)),
|
|
|
|
[{Key, _Conn}] = maps:to_list(get_ldap_connections(Config)),
|
|
|
|
%% Key will be {IsAnon, Servers, Options}
|
|
%% IsAnon is false for password authorization
|
|
{false, _, _} = Key.
|
|
|
|
|
|
get_ldap_connections(Config) ->
|
|
rabbit_ct_broker_helpers:rpc(Config, 0,
|
|
rabbit_auth_backend_ldap, get_connections, []).
|
|
|
|
ldap_only(Config) ->
|
|
ok = rabbit_ct_broker_helpers:rpc(Config, 0,
|
|
application, set_env, [rabbit, auth_backends, [rabbit_auth_backend_ldap]]),
|
|
login(Config),
|
|
in_group(Config),
|
|
const(Config),
|
|
string_match(Config),
|
|
boolean_logic(Config),
|
|
tag_check(Config, [monitor]),
|
|
tag_check_subst(Config),
|
|
logging(Config),
|
|
ok.
|
|
|
|
ldap_and_internal(Config) ->
|
|
ok = rabbit_ct_broker_helpers:rpc(Config, 0,
|
|
application, set_env, [rabbit, auth_backends,
|
|
[{rabbit_auth_backend_ldap, rabbit_auth_backend_internal}]]),
|
|
login(Config),
|
|
permission_match(Config),
|
|
tag_check(Config, [monitor, management, foo]),
|
|
ok.
|
|
|
|
internal_followed_ldap_and_internal(Config) ->
|
|
ok = rabbit_ct_broker_helpers:rpc(Config, 0,
|
|
application, set_env, [rabbit, auth_backends,
|
|
[rabbit_auth_backend_internal, {rabbit_auth_backend_ldap, rabbit_auth_backend_internal}]]),
|
|
login(Config),
|
|
permission_match(Config),
|
|
tag_check(Config, [monitor, management, foo]),
|
|
ok.
|
|
|
|
tag_attribution_ldap_only(Config) ->
|
|
set_env(Config, tag_query_configuration()),
|
|
ok = rabbit_ct_broker_helpers:rpc(Config, 0,
|
|
application, set_env, [rabbit, auth_backends, [rabbit_auth_backend_ldap]]),
|
|
tag_check(Config, <<"Edward">>, <<"password">>, [monitor, normal]).
|
|
|
|
tag_attribution_ldap_and_internal(Config) ->
|
|
set_env(Config, tag_query_configuration()),
|
|
ok = rabbit_ct_broker_helpers:rpc(Config, 0,
|
|
application, set_env, [rabbit, auth_backends, [{rabbit_auth_backend_ldap,
|
|
rabbit_auth_backend_internal}]]),
|
|
tag_check(Config, <<"Edward">>, <<"password">>,
|
|
[monitor, normal] ++ internal_authorization_tags()).
|
|
|
|
tag_attribution_internal_followed_by_ldap_and_internal(Config) ->
|
|
set_env(Config, tag_query_configuration()),
|
|
ok = rabbit_ct_broker_helpers:rpc(Config, 0,
|
|
application, set_env, [rabbit, auth_backends, [rabbit_auth_backend_internal,
|
|
{rabbit_auth_backend_ldap,
|
|
rabbit_auth_backend_internal}]]),
|
|
tag_check(Config, <<"Edward">>, <<"password">>,
|
|
[monitor, normal] ++ internal_authorization_tags()).
|
|
|
|
invalid_or_clause_ldap_only(Config) ->
|
|
set_env(Config, vhost_access_query_or_in_group()),
|
|
ok = rabbit_ct_broker_helpers:rpc(Config, 0,
|
|
application, set_env, [rabbit, auth_backends, [rabbit_auth_backend_ldap]]),
|
|
B = #amqp_params_network{port = rabbit_ct_broker_helpers:get_node_config(Config, 0, tcp_port_amqp)},
|
|
{ok, C} = amqp_connection:start(B?ALICE),
|
|
ok = amqp_connection:close(C).
|
|
|
|
invalid_and_clause_ldap_only(Config) ->
|
|
set_env(Config, vhost_access_query_and_in_group()),
|
|
ok = rabbit_ct_broker_helpers:rpc(Config, 0,
|
|
application, set_env, [rabbit, auth_backends, [rabbit_auth_backend_ldap]]),
|
|
B = #amqp_params_network{port = rabbit_ct_broker_helpers:get_node_config(Config, 0, tcp_port_amqp)},
|
|
% NB: if the query crashes the ldap plugin it returns {error, access_refused}
|
|
% This may not be a reliable return value assertion
|
|
{error, not_allowed} = amqp_connection:start(B?ALICE).
|
|
|
|
topic_authorisation_publishing_ldap_only(Config) ->
|
|
%% topic authorisation at publishing time is enforced in the AMQP channel
|
|
%% so it can be tested by sending messages
|
|
ok = rabbit_ct_broker_helpers:rpc(Config, 0,
|
|
application, set_env, [rabbit, auth_backends, [rabbit_auth_backend_ldap]]),
|
|
|
|
%% default is to let pass
|
|
P = #amqp_params_network{port = rabbit_ct_broker_helpers:get_node_config(Config, 0, tcp_port_amqp)},
|
|
test_publish(P?ALICE, <<"amq.topic">>, <<"a.b.c">>, ok),
|
|
|
|
%% let pass for topic
|
|
set_env(Config, [{topic_access_query, {constant, true}}]),
|
|
|
|
P = #amqp_params_network{port = rabbit_ct_broker_helpers:get_node_config(Config, 0, tcp_port_amqp)},
|
|
test_publish(P?ALICE, <<"amq.topic">>, <<"a.b.c">>, ok),
|
|
|
|
%% check string substitution (on username)
|
|
set_env(Config, [{topic_access_query, {for, [{permission, write, {equals, "${username}", "Alice"}},
|
|
{permission, read, {constant, false}}
|
|
]}}]),
|
|
test_publish(P?ALICE, <<"amq.topic">>, <<"a.b.c">>, ok),
|
|
test_publish(P?BOB, <<"amq.topic">>, <<"a.b.c">>, fail),
|
|
|
|
%% check string substitution on routing key (with regex)
|
|
set_env(Config, [{topic_access_query, {for, [{permission, write, {'and',
|
|
[{equals, "${username}", "Alice"},
|
|
{match, {string, "${routing_key}"}, {string, "^a"}}]
|
|
}},
|
|
{permission, read, {constant, false}}
|
|
]}}]),
|
|
%% user and routing key OK
|
|
test_publish(P?ALICE, <<"amq.topic">>, <<"a.b.c">>, ok),
|
|
%% user and routing key OK
|
|
test_publish(P?ALICE, <<"amq.topic">>, <<"a.c">>, ok),
|
|
%% user OK, routing key KO, should fail
|
|
test_publish(P?ALICE, <<"amq.topic">>, <<"b.c">>, fail),
|
|
%% user KO, routing key OK, should fail
|
|
test_publish(P?BOB, <<"amq.topic">>, <<"a.b.c">>, fail),
|
|
|
|
ok.
|
|
|
|
topic_authorisation_consumption(Config) ->
|
|
%% topic authorisation for consumption isn't enforced in AMQP
|
|
%% (it is in plugins like STOMP and MQTT, at subscription time)
|
|
%% so we directly test the LDAP backend, inside the broker
|
|
ok = rabbit_ct_broker_helpers:rpc(Config, 0,
|
|
?MODULE, topic_authorisation_consumption1, [Config]).
|
|
|
|
topic_authorisation_consumption1(Config) ->
|
|
%% we can't use the LDAP backend record here, falling back to simple tuples
|
|
Alice = {auth_user,<<"Alice">>, [monitor],
|
|
fun() -> {impl,"cn=Alice,ou=People,dc=rabbitmq,dc=com",<<"password">>} end
|
|
},
|
|
Bob = {auth_user,<<"Bob">>, [monitor],
|
|
fun() -> {impl,"cn=Bob,ou=People,dc=rabbitmq,dc=com",<<"password">>} end
|
|
},
|
|
Resource = #resource{virtual_host = <<"/">>, name = <<"amq.topic">>, kind = topic},
|
|
Context = #{routing_key => <<"a.b">>,
|
|
variable_map => #{
|
|
<<"username">> => <<"guest">>,
|
|
<<"vhost">> => <<"other-vhost">>
|
|
}},
|
|
%% default is to let pass
|
|
true = rabbit_auth_backend_ldap:check_topic_access(Alice, Resource, read, Context),
|
|
|
|
%% let pass for topic
|
|
set_env(Config, [{topic_access_query, {for, [{permission, read, {constant, true}},
|
|
{permission, write, {constant, false}}]
|
|
}}]),
|
|
|
|
true = rabbit_auth_backend_ldap:check_topic_access(Alice, Resource, read, Context),
|
|
|
|
%% check string substitution (on username)
|
|
set_env(Config, [{topic_access_query, {for, [{permission, read, {equals, "${username}", "Alice"}},
|
|
{permission, write, {constant, false}}]
|
|
}}]),
|
|
|
|
true = rabbit_auth_backend_ldap:check_topic_access(Alice, Resource, read, Context),
|
|
false = rabbit_auth_backend_ldap:check_topic_access(Bob, Resource, read, Context),
|
|
|
|
%% check string substitution on routing key (with regex)
|
|
set_env(Config, [{topic_access_query, {for, [{permission, read, {'and',
|
|
[{equals, "${username}", "Alice"},
|
|
{match, {string, "${routing_key}"}, {string, "^a"}}]
|
|
}},
|
|
{permission, write, {constant, false}}]
|
|
}}]),
|
|
%% user and routing key OK
|
|
true = rabbit_auth_backend_ldap:check_topic_access(Alice, Resource, read, #{routing_key => <<"a.b.c">>}),
|
|
%% user and routing key OK
|
|
true = rabbit_auth_backend_ldap:check_topic_access(Alice, Resource, read, #{routing_key => <<"a.c">>}),
|
|
%% user OK, routing key KO, should fail
|
|
false = rabbit_auth_backend_ldap:check_topic_access(Alice, Resource, read, #{routing_key => <<"b.c">>}),
|
|
%% user KO, routing key OK, should fail
|
|
false = rabbit_auth_backend_ldap:check_topic_access(Bob, Resource, read, #{routing_key => <<"a.b.c">>}),
|
|
ok.
|
|
|
|
match_bidirectional(Config) ->
|
|
ok = rabbit_ct_broker_helpers:rpc(Config, 0,
|
|
application, set_env, [rabbit, auth_backends, [rabbit_auth_backend_ldap]]),
|
|
|
|
Configurations = [
|
|
fun resource_access_query_match/0,
|
|
fun resource_access_query_match_query_is_string/0,
|
|
fun resource_access_query_match_re_query_is_string/0,
|
|
fun resource_access_query_match_query_and_re_query_are_strings/0
|
|
],
|
|
|
|
[begin
|
|
set_env(Config, ConfigurationFunction()),
|
|
Q1 = [#'queue.declare'{queue = <<"Alice-queue">>}],
|
|
Q2 = [#'queue.declare'{queue = <<"Ali">>}],
|
|
P = #amqp_params_network{port = rabbit_ct_broker_helpers:get_node_config(Config, 0, tcp_port_amqp)},
|
|
[test_resource(PTR) || PTR <- [{P?ALICE, Q1, ok},
|
|
{P?ALICE, Q2, fail}]]
|
|
end || ConfigurationFunction <- Configurations],
|
|
ok.
|
|
|
|
match_bidirectional_gh_100(Config) ->
|
|
ok = rabbit_ct_broker_helpers:rpc(Config, 0,
|
|
application, set_env, [rabbit, auth_backends, [rabbit_auth_backend_ldap]]),
|
|
|
|
Configurations = [
|
|
fun resource_access_query_match_gh_100/0,
|
|
fun resource_access_query_match_query_is_string_gh_100/0
|
|
],
|
|
|
|
[begin
|
|
set_env(Config, ConfigurationFunction()),
|
|
Q1 = [#'queue.declare'{queue = <<"Jimmy-queue">>}],
|
|
Q2 = [#'queue.declare'{queue = <<"Jimmy">>}],
|
|
P = #amqp_params_network{port = rabbit_ct_broker_helpers:get_node_config(Config, 0, tcp_port_amqp)},
|
|
[test_resource(PTR) || PTR <- [{P?JIMMY, Q1, ok},
|
|
{P?JIMMY, Q2, ok}]]
|
|
end || ConfigurationFunction <- Configurations],
|
|
ok.
|
|
|
|
%%--------------------------------------------------------------------
|
|
|
|
test_publish(Person, Exchange, RoutingKey, ExpectedResult) ->
|
|
{ok, Connection} = amqp_connection:start(Person),
|
|
{ok, Channel} = amqp_connection:open_channel(Connection),
|
|
ActualResult =
|
|
try
|
|
Publish = #'basic.publish'{exchange = Exchange, routing_key = RoutingKey},
|
|
amqp_channel:cast(Channel, Publish, #amqp_msg{payload = <<"foobar">>}),
|
|
amqp_channel:call(Channel, #'basic.qos'{prefetch_count = 0}),
|
|
ok
|
|
catch exit:_ -> fail
|
|
after
|
|
amqp_connection:close(Connection)
|
|
end,
|
|
ExpectedResult = ActualResult.
|
|
|
|
login(Config) ->
|
|
lists:flatten(
|
|
[test_login(Config, {N, Env}, L, FilterList, case {LGood, EnvGood} of
|
|
{good, good} -> fun succ/1;
|
|
_ -> fun fail/1
|
|
end) ||
|
|
{LGood, FilterList, L, _Tags} <- logins(Config),
|
|
{N, {EnvGood, Env}} <- login_envs()]).
|
|
|
|
logins(Config) -> logins_network(Config) ++ logins_direct(Config).
|
|
|
|
%% Format for login tests, {Outcome, FilterList, Login, Tags}.
|
|
%% Tests skipped for each login_env reference in FilterList.
|
|
logins_network(Config) ->
|
|
B = #amqp_params_network{port = rabbit_ct_broker_helpers:get_node_config(Config, 0, tcp_port_amqp)},
|
|
[{bad, [5, 6], B#amqp_params_network{}, []},
|
|
{bad, [5, 6], B#amqp_params_network{username = <<?ALICE_NAME>>}, []},
|
|
{bad, [5, 6], B#amqp_params_network{username = <<?ALICE_NAME>>,
|
|
password = <<"password">>}, []},
|
|
{bad, [5, 6], B#amqp_params_network{username = <<"Alice">>,
|
|
password = <<"Alicja">>,
|
|
virtual_host = <<?VHOST>>}, []},
|
|
{bad, [1, 2, 3, 4, 6, 7], B?CAROL, []},
|
|
{good, [5, 6], B?ALICE, []},
|
|
{good, [5, 6], B?BOB, []},
|
|
{good, [1, 2, 3, 4, 6, 7, 8], B?PETER, []}].
|
|
|
|
logins_direct(Config) ->
|
|
N = #amqp_params_direct{node = rabbit_ct_broker_helpers:get_node_config(Config, 0, nodename)},
|
|
[{bad, [5], N#amqp_params_direct{}, []},
|
|
{bad, [5], N#amqp_params_direct{username = <<?ALICE_NAME>>}, []},
|
|
{bad, [5], N#amqp_params_direct{username = <<?ALICE_NAME>>,
|
|
password = <<"password">>}, [management]},
|
|
{good, [5], N#amqp_params_direct{username = <<?ALICE_NAME>>,
|
|
password = <<"password">>,
|
|
virtual_host = <<?VHOST>>}, [management]}].
|
|
|
|
%% Format for login envs, {Reference, {Outcome, Env}}
|
|
login_envs() ->
|
|
[{1, {good, base_login_env()}},
|
|
{2, {good, dn_lookup_pre_bind_env()}},
|
|
{3, {good, other_bind_admin_env()}},
|
|
{4, {good, other_bind_anon_env()}},
|
|
{5, {good, posix_vhost_access_multiattr_env()}},
|
|
{6, {good, tag_queries_subst_env()}},
|
|
{7, {bad, other_bind_broken_env()}},
|
|
{8, {good, vhost_access_query_nested_groups_env()}}].
|
|
|
|
base_login_env() ->
|
|
[{user_dn_pattern, "cn=${username},ou=People,dc=rabbitmq,dc=com"},
|
|
{dn_lookup_attribute, none},
|
|
{dn_lookup_base, none},
|
|
{dn_lookup_bind, as_user},
|
|
{other_bind, as_user},
|
|
{tag_queries, [{monitor, {constant, true}},
|
|
{administrator, {constant, false}},
|
|
{management, {constant, false}}]},
|
|
{vhost_access_query, {exists, "ou=${vhost},ou=vhosts,dc=rabbitmq,dc=com"}},
|
|
{log, true}].
|
|
|
|
%% TODO configure OpenLDAP to allow a dn_lookup_post_bind_env()
|
|
dn_lookup_pre_bind_env() ->
|
|
[{user_dn_pattern, "${username}"},
|
|
{dn_lookup_attribute, "cn"},
|
|
{dn_lookup_base, "OU=People,DC=rabbitmq,DC=com"},
|
|
{dn_lookup_bind, {"cn=admin,dc=rabbitmq,dc=com", "admin"}}].
|
|
|
|
other_bind_admin_env() ->
|
|
[{other_bind, {"cn=admin,dc=rabbitmq,dc=com", "admin"}}].
|
|
|
|
other_bind_anon_env() ->
|
|
[{other_bind, anon}].
|
|
|
|
other_bind_broken_env() ->
|
|
[{other_bind, {"cn=admin,dc=rabbitmq,dc=com", "admi"}}].
|
|
|
|
tag_queries_subst_env() ->
|
|
[{tag_queries, [{administrator, {constant, false}},
|
|
{management,
|
|
{exists, "ou=${vhost},ou=vhosts,dc=rabbitmq,dc=com"}}]}].
|
|
|
|
posix_vhost_access_multiattr_env() ->
|
|
[{user_dn_pattern, "uid=${username},ou=People,dc=rabbitmq,dc=com"},
|
|
{vhost_access_query,
|
|
{'and', [{exists, "ou=${vhost},ou=vhosts,dc=rabbitmq,dc=com"},
|
|
{equals,
|
|
{attribute, "${user_dn}","memberOf"},
|
|
{string, "cn=wheel,ou=groups,dc=rabbitmq,dc=com"}},
|
|
{equals,
|
|
{attribute, "${user_dn}","memberOf"},
|
|
{string, "cn=people,ou=groups,dc=rabbitmq,dc=com"}},
|
|
{equals,
|
|
{string, "cn=wheel,ou=groups,dc=rabbitmq,dc=com"},
|
|
{attribute,"${user_dn}","memberOf"}},
|
|
{equals,
|
|
{string, "cn=people,ou=groups,dc=rabbitmq,dc=com"},
|
|
{attribute, "${user_dn}","memberOf"}},
|
|
{match,
|
|
{attribute, "${user_dn}","memberOf"},
|
|
{string, "cn=wheel,ou=groups,dc=rabbitmq,dc=com"}},
|
|
{match,
|
|
{attribute, "${user_dn}","memberOf"},
|
|
{string, "cn=people,ou=groups,dc=rabbitmq,dc=com"}},
|
|
{match,
|
|
{string, "cn=wheel,ou=groups,dc=rabbitmq,dc=com"},
|
|
{attribute, "${user_dn}","memberOf"}},
|
|
{match,
|
|
{string, "cn=people,ou=groups,dc=rabbitmq,dc=com"},
|
|
{attribute, "${user_dn}","memberOf"}}
|
|
]}}].
|
|
|
|
vhost_access_query_or_in_group() ->
|
|
[{vhost_access_query,
|
|
{'or', [
|
|
{in_group, "cn=bananas,ou=groups,dc=rabbitmq,dc=com"},
|
|
{in_group, "cn=apples,ou=groups,dc=rabbitmq,dc=com"},
|
|
{in_group, "cn=wheel,ou=groups,dc=rabbitmq,dc=com"}
|
|
]}}].
|
|
|
|
vhost_access_query_and_in_group() ->
|
|
[{vhost_access_query,
|
|
{'and', [
|
|
{in_group, "cn=bananas,ou=groups,dc=rabbitmq,dc=com"},
|
|
{in_group, "cn=wheel,ou=groups,dc=rabbitmq,dc=com"}
|
|
]}}].
|
|
|
|
vhost_access_query_nested_groups_env() ->
|
|
[{vhost_access_query, {in_group_nested, "cn=admins,ou=groups,dc=rabbitmq,dc=com"}}].
|
|
|
|
vhost_access_query_base_env() ->
|
|
[{vhost_access_query, vhost_access_query_base()}].
|
|
|
|
vhost_access_query_base() ->
|
|
{exists, "ou=${vhost},ou=vhosts,dc=rabbitmq,dc=com"}.
|
|
|
|
resource_access_query_match_gh_100() ->
|
|
[{resource_access_query,
|
|
{match, {string, "RMQ-${vhost}"}, {attribute, "${user_dn}", "description"}}
|
|
}].
|
|
|
|
resource_access_query_match_query_is_string_gh_100() ->
|
|
[{resource_access_query,
|
|
{match, "RMQ-${vhost}", {attribute, "${user_dn}", "description"}}
|
|
}].
|
|
|
|
resource_access_query_match() ->
|
|
[{resource_access_query, {match, {string, "${name}"},
|
|
{string, "^${username}-"}}
|
|
}].
|
|
|
|
resource_access_query_match_query_is_string() ->
|
|
[{resource_access_query, {match, "${name}",
|
|
{string, "^${username}-"}}
|
|
}].
|
|
|
|
resource_access_query_match_re_query_is_string() ->
|
|
[{resource_access_query, {match, {string, "${name}"},
|
|
"^${username}-"}
|
|
}].
|
|
|
|
resource_access_query_match_query_and_re_query_are_strings() ->
|
|
[{resource_access_query, {match, "${name}",
|
|
"^${username}-"}
|
|
}].
|
|
|
|
topic_access_query_base_env() ->
|
|
[{topic_access_query, topic_access_query_base()}].
|
|
|
|
topic_access_query_base() ->
|
|
{constant, true}.
|
|
|
|
test_login(Config, {N, Env}, Login, FilterList, ResultFun) ->
|
|
case lists:member(N, FilterList) of
|
|
true -> [];
|
|
_ ->
|
|
try
|
|
set_env(Config, Env),
|
|
ResultFun(Login)
|
|
after
|
|
set_env(Config, base_login_env())
|
|
end
|
|
end.
|
|
|
|
rpc_set_env(Config, Args) ->
|
|
rabbit_ct_broker_helpers:rpc(Config, 0, application, set_env, Args).
|
|
|
|
set_env(Config, Env) ->
|
|
[rpc_set_env(Config, [rabbitmq_auth_backend_ldap, K, V]) || {K, V} <- Env].
|
|
|
|
succ(Login) ->
|
|
{ok, Pid} = amqp_connection:start(Login),
|
|
amqp_connection:close(Pid).
|
|
fail(Login) -> ?assertMatch({error, _}, amqp_connection:start(Login)).
|
|
|
|
%%--------------------------------------------------------------------
|
|
|
|
in_group(Config) ->
|
|
X = [#'exchange.declare'{exchange = <<"test">>}],
|
|
B = #amqp_params_network{port = rabbit_ct_broker_helpers:get_node_config(Config, 0, tcp_port_amqp)},
|
|
test_resources([{B?ALICE, X, ok},
|
|
{B?BOB, X, fail}]).
|
|
|
|
const(Config) ->
|
|
Q = [#'queue.declare'{queue = <<"test">>}],
|
|
B = #amqp_params_network{port = rabbit_ct_broker_helpers:get_node_config(Config, 0, tcp_port_amqp)},
|
|
test_resources([{B?ALICE, Q, ok},
|
|
{B?BOB, Q, fail}]).
|
|
|
|
string_match(Config) ->
|
|
B = fun(N) ->
|
|
[#'exchange.declare'{exchange = N},
|
|
#'queue.declare'{queue = <<"test">>},
|
|
#'queue.bind'{exchange = N, queue = <<"test">>}]
|
|
end,
|
|
P = #amqp_params_network{port = rabbit_ct_broker_helpers:get_node_config(Config, 0, tcp_port_amqp)},
|
|
test_resources([{P?ALICE, B(<<"xch-Alice-abc123">>), ok},
|
|
{P?ALICE, B(<<"abc123">>), fail},
|
|
{P?ALICE, B(<<"xch-Someone Else-abc123">>), fail}]).
|
|
|
|
boolean_logic(Config) ->
|
|
Q1 = [#'queue.declare'{queue = <<"test1">>},
|
|
#'basic.consume'{queue = <<"test1">>}],
|
|
Q2 = [#'queue.declare'{queue = <<"test2">>},
|
|
#'basic.consume'{queue = <<"test2">>}],
|
|
P = #amqp_params_network{port = rabbit_ct_broker_helpers:get_node_config(Config, 0, tcp_port_amqp)},
|
|
[test_resource(PTR) || PTR <- [{P?ALICE, Q1, ok},
|
|
{P?ALICE, Q2, ok},
|
|
{P?BOB, Q1, fail},
|
|
{P?BOB, Q2, fail}]].
|
|
|
|
permission_match(Config) ->
|
|
B = fun(N) ->
|
|
[#'exchange.declare'{exchange = N},
|
|
#'queue.declare'{queue = <<"prefix-test">>},
|
|
#'queue.bind'{exchange = N, queue = <<"prefix-test">>}]
|
|
end,
|
|
P = #amqp_params_network{port = rabbit_ct_broker_helpers:get_node_config(Config, 0, tcp_port_amqp)},
|
|
test_resources([{P?ALICE, B(<<"prefix-abc123">>), ok},
|
|
{P?ALICE, B(<<"abc123">>), fail},
|
|
{P?ALICE, B(<<"xch-Alice-abc123">>), fail}]).
|
|
|
|
%% Tag check tests, with substitution
|
|
tag_check_subst(Config) ->
|
|
lists:flatten(
|
|
[test_tag_check(Config, tag_queries_subst_env(),
|
|
fun () -> tag_check(Config, Username, Password, VHost, Outcome, Tags) end) ||
|
|
{Outcome, _FilterList, #amqp_params_direct{username = Username,
|
|
password = Password,
|
|
virtual_host = VHost},
|
|
Tags} <- logins_direct(Config)]).
|
|
|
|
%% Tag check
|
|
tag_check(Config, Tags) ->
|
|
tag_check(Config, <<?ALICE_NAME>>, <<"password">>, Tags).
|
|
|
|
tag_check(Config, Username, Password, Tags) ->
|
|
tag_check(Config, Username, Password, <<>>, good, Tags).
|
|
|
|
tag_check(Config, Username, Password, VHost, Outcome, Tags)
|
|
when is_binary(Username), is_binary(Password), is_binary(VHost), is_list(Tags) ->
|
|
{ok, User} = rabbit_ct_broker_helpers:rpc(Config, 0, rabbit_access_control, check_user_login, [Username, [{password, Password}, {vhost, VHost}]]),
|
|
tag_check_outcome(Outcome, Tags, User);
|
|
tag_check(_, _, _, _, _, _) -> fun() -> [] end.
|
|
|
|
tag_check_outcome(good, Tags, User) -> ?assertEqual(Tags, User#user.tags);
|
|
tag_check_outcome(bad, Tags, User) -> ?assertNotEqual(Tags, User#user.tags).
|
|
|
|
test_tag_check(Config, Env, TagCheckFun) ->
|
|
try
|
|
set_env(Config, Env),
|
|
TagCheckFun()
|
|
after
|
|
set_env(Config, base_login_env())
|
|
end.
|
|
|
|
tag_query_configuration() ->
|
|
[{tag_queries,
|
|
[{administrator, {constant, false}},
|
|
%% Query result for tag `management` is FALSE
|
|
%% because this object does NOT exist.
|
|
{management,
|
|
{exists, "cn=${username},ou=Faculty,dc=Computer Science,dc=Engineering"}},
|
|
{monitor, {constant, true}},
|
|
%% Query result for tag `normal` is TRUE because
|
|
%% this object exists.
|
|
{normal,
|
|
{exists, "cn=${username},ou=people,dc=rabbitmq,dc=com"}}]}].
|
|
|
|
internal_authorization_setup(Config) ->
|
|
ok = control_action(Config, add_user, ["Edward", ""]),
|
|
ok = control_action(Config, set_user_tags, ["Edward"] ++
|
|
[ atom_to_list(T) || T <- internal_authorization_tags() ]).
|
|
|
|
internal_authorization_teardown(Config) ->
|
|
control_action(Config, delete_user, ["Edward"]).
|
|
|
|
internal_authorization_tags() ->
|
|
[foo, bar].
|
|
|
|
%% Logging tests, triggered within 'test_login/4'
|
|
logging(Config) ->
|
|
lists:flatten(
|
|
[test_login(Config, {N, Env}, L, FilterList, case {LGood, EnvGood} of
|
|
{good, good} -> fun succ/1;
|
|
_ -> fun fail/1
|
|
end) ||
|
|
{LGood, FilterList, L} <- logging_test_users(Config),
|
|
{N, {EnvGood, Env}} <- logging_envs()]).
|
|
|
|
%% Format for logging tests, {Outcome, FilterList, Login}.
|
|
logging_test_users(Config) ->
|
|
P = #amqp_params_network{port = rabbit_ct_broker_helpers:get_node_config(Config, 0, tcp_port_amqp)},
|
|
[{bad, [], P#amqp_params_network{username = <<?ALICE_NAME>>}},
|
|
{good, [], P?ALICE}].
|
|
|
|
logging_envs() ->
|
|
[{1, {good, scrub_bind_creds_env()}},
|
|
{2, {good, display_bind_creds_env()}},
|
|
{3, {bad, scrub_bind_single_cred_env()}},
|
|
{4, {bad, scrub_bind_creds_no_equals_env()}},
|
|
{5, {bad, scrub_bind_creds_no_seperator_env()}}].
|
|
|
|
scrub_bind_creds_env() ->
|
|
[{log, network},
|
|
{other_bind, {"cn=admin,dc=rabbitmq,dc=com", "admin"}}].
|
|
|
|
display_bind_creds_env() ->
|
|
[{log, network_unsafe},
|
|
{other_bind, {"cn=admin,dc=rabbitmq,dc=com", "admin"}}].
|
|
|
|
scrub_bind_single_cred_env() ->
|
|
[{log, network},
|
|
{other_bind, {"dc=com", "admin"}}].
|
|
|
|
scrub_bind_creds_no_equals_env() ->
|
|
[{log, network},
|
|
{other_bind, {"cn*admin,dc>rabbitmq,dc&com", "admin"}}].
|
|
|
|
scrub_bind_creds_no_seperator_env() ->
|
|
[{log, network},
|
|
{other_bind, {"cn=admindc=rabbitmqdc&com", "admin"}}].
|
|
|
|
%%--------------------------------------------------------------------
|
|
|
|
test_resources(PTRs) -> [test_resource(PTR) || PTR <- PTRs].
|
|
|
|
test_resource({Person, Things, Result}) ->
|
|
{ok, Conn} = amqp_connection:start(Person),
|
|
{ok, Ch} = amqp_connection:open_channel(Conn),
|
|
?assertEqual(Result,
|
|
try
|
|
[amqp_channel:call(Ch, T) || T <- Things],
|
|
ok
|
|
catch exit:_ -> fail
|
|
after
|
|
amqp_connection:close(Conn)
|
|
end).
|
|
|
|
control_action(Config, Command, Args) ->
|
|
Node = rabbit_ct_broker_helpers:get_node_config(Config, 0, nodename),
|
|
control_action(Config, Command, Node, Args, default_options()).
|
|
|
|
control_action(Config, Command, Args, NewOpts) ->
|
|
Node = rabbit_ct_broker_helpers:get_node_config(Config, 0, nodename),
|
|
control_action(Config, Command, Node, Args,
|
|
expand_options(default_options(), NewOpts)).
|
|
|
|
control_action(_Config, Command, Node, Args, Opts) ->
|
|
case rabbit_control_helper:command(Command, Node, Args, Opts) of
|
|
ok ->
|
|
io:format("done.~n"),
|
|
ok;
|
|
Other ->
|
|
io:format("failed.~n"),
|
|
Other
|
|
end.
|
|
|
|
default_options() -> [{"-p", ?VHOST}, {"-q", "false"}].
|
|
|
|
expand_options(As, Bs) ->
|
|
lists:foldl(fun({K, _}=A, R) ->
|
|
case proplists:is_defined(K, R) of
|
|
true -> R;
|
|
false -> [A | R]
|
|
end
|
|
end, Bs, As).
|