Skip to content

Propagate all credentials to http backend #10200

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Dec 24, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions deps/rabbitmq_auth_backend_http/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@ against the URIs listed in the configuration file. It will add query string
* `username`: the name of the user
* `password`: the password provided (may be missing if e.g. rabbitmq-auth-mechanism-ssl is used)

Note: This request may include additional http request parameters in addition to the ones listed above.
For instance, if the user accessed RabbitMQ via the MQTT protocol, it is expected `client_id` and `vhost` request parameters too.

### vhost_path

* `username`: the name of the user
Expand All @@ -100,6 +103,9 @@ Note that you cannot create arbitrary virtual hosts using this plugin; you can o
* `name`: the name of the resource
* `permission`:the access level to the resource (`configure`, `write`, `read`): see [the Access Control guide](http://www.rabbitmq.com/access-control.html) for their meaning

Note: This request may include additional http request parameters in addition to the ones listed above.
For instance, if the user accessed RabbitMQ via the MQTT protocol, it is expected `client_id` request parameter too.

### topic_path

* `username`: the name of the user
Expand Down
54 changes: 37 additions & 17 deletions deps/rabbitmq_auth_backend_http/src/rabbit_auth_backend_http.erl
Original file line number Diff line number Diff line change
Expand Up @@ -34,33 +34,53 @@ description() ->

user_login_authentication(Username, AuthProps) ->

case http_req(p(user_path), q([{username, Username}|extractPassword(AuthProps)])) of
case http_req(p(user_path), q([{username, Username}]++extractOtherCredentials(AuthProps))) of
{error, _} = E -> E;
"deny" -> {refused, "Denied by the backing HTTP service", []};
"allow" ++ Rest -> Tags = [rabbit_data_coercion:to_atom(T) ||
T <- string:tokens(Rest, " ")],

{ok, #auth_user{username = Username,
tags = Tags,
impl = fun() -> proplists:get_value(password, AuthProps, none) end}};
impl = fun() -> proplists:delete(username, AuthProps) end}};
Other -> {error, {bad_response, Other}}
end.

%% Credentials (i.e. password) maybe directly in the password attribute in AuthProps
%% or as a Function with the attribute rabbit_auth_backend_http if the user was already authenticated with http backend
%% or as a Function with the attribute rabbit_auth_backend_cache if the user was already authenticated via cache backend
extractPassword(AuthProps) ->
case proplists:get_value(password, AuthProps, none) of
none ->
case proplists:get_value(rabbit_auth_backend_http, AuthProps, none) of
none -> case proplists:get_value(rabbit_auth_backend_cache, AuthProps, none) of
none -> [];
PasswordFun -> [{password, PasswordFun()}]
end;
PasswordFun -> [{password, PasswordFun()}]
end;
Password -> [{password, Password}]
end.
%% When some protocols, such as MQTT, uses an internal AMQP client, to interact with RabbitMQ core,
%% it happens that the main protocol authenticates the user passing all credentials (e.g. password, client_id, vhost)
%% however the internal AMQP client also performs further authentications. THe latter authentication
%% attempt lacks of all credentials. Instead those credentials are persisted behind a function call
%% that returns an AuthProps.
%% If the user was first authenticated by rabbit_auth_backend_http, there will be one property called
%% `rabbit_auth_backend_http` whose value is a function that returns a proplist with all the credentials used
%% on the first successful login.
%% However, it may happen that the user was authenticated via rabbit_auth_backend_cache, in that case,
%% the property `rabbit_auth_backend_cache` is a function which returns a proplist with all the credentials used
%% on the first succcessful login.
resolveUsingPersistedCredentials(AuthProps) ->
case proplists:get_value(rabbit_auth_backend_http, AuthProps, none) of
none -> case proplists:get_value(rabbit_auth_backend_cache, AuthProps, none) of
none -> AuthProps;
CacheAuthPropsFun -> AuthProps ++ CacheAuthPropsFun()
end;
HttpAuthPropsFun -> AuthProps ++ HttpAuthPropsFun()
end.


%% Some protocols may add additional credentials into the AuthProps that should be propagated to
%% the external authentication backends
%% This function excludes any attribute that starts with rabbit_auth_backend_
is_internal_property(rabbit_auth_backend_http) -> true;
is_internal_property(rabbit_auth_backend_cache) -> true;
is_internal_property(_Other) -> false.

extractOtherCredentials(AuthProps) ->
PublicAuthProps = [{K,V} || {K,V} <-AuthProps, not is_internal_property(K)],
case PublicAuthProps of
[] -> resolveUsingPersistedCredentials(AuthProps);
_ -> PublicAuthProps
end.


user_login_authorization(Username, AuthProps) ->
case user_login_authentication(Username, AuthProps) of
Expand Down
69 changes: 61 additions & 8 deletions deps/rabbitmq_auth_backend_http/test/auth_SUITE.erl
Original file line number Diff line number Diff line change
Expand Up @@ -20,33 +20,86 @@
{vhost_path, "http://localhost:" ++ integer_to_list(?AUTH_PORT) ++ "/auth/vhost"},
{resource_path, "http://localhost:" ++ integer_to_list(?AUTH_PORT) ++ "/auth/resource"},
{topic_path, "http://localhost:" ++ integer_to_list(?AUTH_PORT) ++ "/auth/topic"}]).
-define(ALLOWED_USER, #{username => <<"Ala">>,
-define(ALLOWED_USER, #{username => <<"Ala1">>,
password => <<"Kocur">>,
expected_credentials => [username, password],
tags => [policymaker, monitoring]}).
-define(DENIED_USER, #{username => <<"Alice">>, password => <<"Cat">>}).
-define(ALLOWED_USER_WITH_EXTRA_CREDENTIALS, #{username => <<"Ala2">>,
password => <<"Kocur">>,
client_id => <<"some_id">>,
expected_credentials => [username, password, client_id],
tags => [policymaker, monitoring]}).
-define(DENIED_USER, #{username => <<"Alice">>,
password => <<"Cat">>
}).

all() -> [grants_access_to_user, denies_access_to_user].
all() -> [grants_access_to_user,
denies_access_to_user,
grants_access_to_user_passing_additional_required_authprops,
grants_access_to_user_skipping_internal_authprops,
grants_access_to_user_with_credentials_in_rabbit_auth_backend_http,
grants_access_to_user_with_credentials_in_rabbit_auth_backend_cache].

init_per_suite(Config) ->
configure_http_auth_backend(),
#{username := Username, password := Password, tags := Tags} = ?ALLOWED_USER,
start_http_auth_server(?AUTH_PORT, ?USER_PATH, #{Username => {Password, Tags}}),
[{allowed_user, ?ALLOWED_USER}, {denied_user, ?DENIED_USER} | Config].
{User1, Tuple1} = extractUserTuple(?ALLOWED_USER),
{User2, Tuple2} = extractUserTuple(?ALLOWED_USER_WITH_EXTRA_CREDENTIALS),
start_http_auth_server(?AUTH_PORT, ?USER_PATH, #{User1 => Tuple1, User2 => Tuple2}),
[{allowed_user, ?ALLOWED_USER},
{allowed_user_with_extra_credentials, ?ALLOWED_USER_WITH_EXTRA_CREDENTIALS},
{denied_user, ?DENIED_USER} | Config].
extractUserTuple(User) ->
#{username := Username, password := Password, tags := Tags, expected_credentials := ExpectedCredentials} = User,
{Username, {Password, Tags, ExpectedCredentials}}.

end_per_suite(_Config) ->
stop_http_auth_server().

grants_access_to_user(Config) ->
#{username := U, password := P, tags := T} = ?config(allowed_user, Config),
{ok, User} = rabbit_auth_backend_http:user_login_authentication(U, [{password, P}]),
?assertMatch({U, T, P},
AuthProps = [{password, P}],
{ok, User} = rabbit_auth_backend_http:user_login_authentication(U, AuthProps),

?assertMatch({U, T, AuthProps},
{User#auth_user.username, User#auth_user.tags, (User#auth_user.impl)()}).

denies_access_to_user(Config) ->
#{username := U, password := P} = ?config(denied_user, Config),
?assertMatch({refused, "Denied by the backing HTTP service", []},
rabbit_auth_backend_http:user_login_authentication(U, [{password, P}])).


grants_access_to_user_passing_additional_required_authprops(Config) ->
#{username := U, password := P, tags := T, client_id := ClientId} = ?config(allowed_user_with_extra_credentials, Config),
AuthProps = [{password, P}, {client_id, ClientId}],
{ok, User} = rabbit_auth_backend_http:user_login_authentication(U, AuthProps),
?assertMatch({U, T, AuthProps},
{User#auth_user.username, User#auth_user.tags, (User#auth_user.impl)()}).

grants_access_to_user_skipping_internal_authprops(Config) ->
#{username := U, password := P, tags := T, client_id := ClientId} = ?config(allowed_user_with_extra_credentials, Config),
AuthProps = [{password, P}, {client_id, ClientId}, {rabbit_any_internal_property, <<"some value">>}],
{ok, User} = rabbit_auth_backend_http:user_login_authentication(U, AuthProps),

?assertMatch({U, T, AuthProps},
{User#auth_user.username, User#auth_user.tags, (User#auth_user.impl)()}).

grants_access_to_user_with_credentials_in_rabbit_auth_backend_http(Config) ->
#{username := U, password := P, tags := T, client_id := ClientId} = ?config(allowed_user_with_extra_credentials, Config),
AuthProps = [{rabbit_auth_backend_http, fun() -> [{password, P}, {client_id, ClientId}] end}],
{ok, User} = rabbit_auth_backend_http:user_login_authentication(U, AuthProps),

?assertMatch({U, T, AuthProps},
{User#auth_user.username, User#auth_user.tags, (User#auth_user.impl)()}).

grants_access_to_user_with_credentials_in_rabbit_auth_backend_cache(Config) ->
#{username := U, password := P, tags := T, client_id := ClientId} = ?config(allowed_user_with_extra_credentials, Config),
AuthProps = [{rabbit_auth_backend_cache, fun() -> [{password, P}, {client_id, ClientId}] end}],
{ok, User} = rabbit_auth_backend_http:user_login_authentication(U, AuthProps),

?assertMatch({U, T, AuthProps},
{User#auth_user.username, User#auth_user.tags, (User#auth_user.impl)()}).

%%% HELPERS

configure_http_auth_backend() ->
Expand Down
22 changes: 13 additions & 9 deletions deps/rabbitmq_auth_backend_http/test/auth_http_mock.erl
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,25 @@

init(Req = #{method := <<"GET">>}, Users) ->
QsVals = cowboy_req:parse_qs(Req),
Reply = authenticate(proplists:get_value(<<"username">>, QsVals),
proplists:get_value(<<"password">>, QsVals),
Users),
Reply = authenticate(QsVals, Users),
Req2 = cowboy_req:reply(200, #{<<"content-type">> => <<"text/plain">>}, Reply, Req),
{ok, Req2, Users}.

%%% HELPERS

authenticate(Username, Password, Users) ->
authenticate(QsVals, Users) ->
Username = proplists:get_value(<<"username">>, QsVals),
Password = proplists:get_value(<<"password">>, QsVals),
case maps:get(Username, Users, undefined) of
{MatchingPassword, Tags} when Password =:= MatchingPassword ->
StringTags = lists:map(fun(T) -> io_lib:format("~ts", [T]) end, Tags),
<<"allow ", (list_to_binary(string:join(StringTags, " ")))/binary>>;
{_OtherPassword, _} ->
{MatchingPassword, Tags, ExpectedCredentials} when Password =:= MatchingPassword ->
case lists:all(fun(C) -> proplists:is_defined(list_to_binary(rabbit_data_coercion:to_list(C)),QsVals) end, ExpectedCredentials) of
true -> StringTags = lists:map(fun(T) -> io_lib:format("~ts", [T]) end, Tags),
<<"allow ", (list_to_binary(string:join(StringTags, " ")))/binary>>;
false -> ct:log("Missing required attributes. Expected ~p, Found: ~p", [ExpectedCredentials, QsVals]),
<<"deny">>
end;
{_OtherPassword, _, _} ->
<<"deny">>;
undefined ->
<<"deny">>
end.
end.
8 changes: 6 additions & 2 deletions deps/rabbitmq_management/selenium/bin/suite_template
Original file line number Diff line number Diff line change
Expand Up @@ -429,9 +429,11 @@ start_fakeproxy() {
init_mock-auth-backend-http() {
AUTH_BACKEND_HTTP_BASEURL=${AUTH_BACKEND_HTTP_BASEURL:-http://localhost:8888}
AUTH_BACKEND_HTTP_DIR=${TEST_CASES_DIR}/mock-auth-backend-http
AUTH_BACKEND_HTTP_EXPECTATIONS=defaultExpectations.json

print "> AUTH_BACKEND_HTTP_BASEURL: ${AUTH_BACKEND_HTTP_BASEURL}"
print "> AUTH_BACKEND_HTTP_DIR: ${AUTH_BACKEND_HTTP_DIR}"
print "> AUTH_BACKEND_HTTP_EXPECTATIONS: ${AUTH_BACKEND_HTTP_EXPECTATIONS}"

}
start_mock-auth-backend-http() {
Expand All @@ -440,16 +442,18 @@ start_mock-auth-backend-http() {
init_mock-auth-backend-http
kill_container_if_exist mock-auth-backend-http

# --env MOCKSERVER_INITIALIZATION_JSON_PATH="/config/$AUTH_BACKEND_HTTP_EXPECTATIONS" \

docker run \
--detach \
--name mock-auth-backend-http \
--net ${DOCKER_NETWORK} \
--publish 8888:1080 \
--env MOCKSERVER_INITIALIZATION_JSON_PATH="/config/expectationInitialiser.json" \
-v ${AUTH_BACKEND_HTTP_DIR}:/config \
mockserver/mockserver

wait_for_url $AUTH_BACKEND_HTTP_BASEURL/ready
#wait_for_url $AUTH_BACKEND_HTTP_BASEURL/ready
wait_for_message mock-auth-backend-http "started on port"
end "mock-auth-backend-http is ready"
}

Expand Down
8 changes: 0 additions & 8 deletions deps/rabbitmq_management/selenium/full-suite-authnz

This file was deleted.

9 changes: 9 additions & 0 deletions deps/rabbitmq_management/selenium/full-suite-authnz-messaging
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
authnz-messaging/auth-cache-http-backends.sh
authnz-messaging/auth-cache-ldap-backends.sh
authnz-messaging/auth-http-backend.sh
authnz-messaging/auth-http-internal-backends-with-internal.sh
authnz-messaging/auth-http-internal-backends.sh
authnz-messaging/auth-internal-backend.sh
authnz-messaging/auth-internal-http-backends.sh
authnz-messaging/auth-ldap-backend.sh
authnz-messaging/auth-http-backend.sh
5 changes: 3 additions & 2 deletions deps/rabbitmq_management/selenium/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
"description": "",
"main": "index.js",
"scripts": {
"test": "mocha --recursive --trace-warnings --timeout 40000",
"fakeportal": "node fakeportal/app.js",
"fakeproxy": "node fakeportal/proxy.js",
"amqp10_roundtriptest" : "export $(cat $ENV_FILE | xargs)&& ./run-amqp10-roundtriptest"
"amqp10_roundtriptest": "eval $(cat $ENV_FILE ) &&./run-amqp10-roundtriptest",
"test": " eval $(cat $ENV_FILE ) && mocha --recursive --trace-warnings --timeout 40000"
},
"keywords": [],
"author": "",
Expand All @@ -18,6 +18,7 @@
"express": "^4.18.2",
"geckodriver": "^3.0.2",
"http-proxy": "^1.18.1",
"mqtt": "^5.3.3",
"path": "^0.12.7",
"proxy": "^1.0.2",
"selenium-webdriver": "^4.4.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const assert = require('assert')
const { getURLForProtocol } = require('../utils')
const { reset, expectUser, expectVhost, expectResource, allow, verifyAll } = require('../mock_http_backend')
const {execSync} = require('child_process')

const profiles = process.env.PROFILES || ""
Expand All @@ -10,17 +11,28 @@ for (const element of profiles.split(" ")) {
}
}

describe('Having the following auth_backends enabled: ' + backends, function () {
describe('Having AMQP 1.0 protocol enabled and the following auth_backends: ' + backends, function () {
let expectations = []

before(function () {

if ( backends.includes("http") ) {
reset()
expectations.push(expectUser({ "username": "httpuser", "password": "httppassword" }, "allow"))
expectations.push(expectVhost({ "username": "httpuser", "vhost": "/"}, "allow"))
expectations.push(expectResource({ "username": "httpuser", "vhost": "/", "resource": "queue", "name": "my-queue", "permission":"configure", "tags":""}, "allow"))
expectations.push(expectResource({ "username": "httpuser", "vhost": "/", "resource": "queue", "name": "my-queue", "permission":"read", "tags":""}, "allow"))
expectations.push(expectResource({ "username": "httpuser", "vhost": "/", "resource": "exchange", "name": "amq.default", "permission":"write", "tags":""}, "allow"))
}
})

it('can open an AMQP 1.0 connection', function () {
execSync("npm run amqp10_roundtriptest")

})

after(function () {

if ( backends.includes("http") ) {
verifyAll(expectations)
}
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[
{
"httpRequest": {
"path": "/ready"
},
"httpResponse": {
"body": "ok"
}
}
]

This file was deleted.

Loading