Merge pull request #722 from rabbitmq/mgmt-oauth

Management UI can obtain an OAuth 2 token from UAA/CF SSO service
This commit is contained in:
Michael Klishin 2019-08-02 19:19:18 +03:00 committed by GitHub
commit f1ade9c9a6
21 changed files with 381 additions and 24 deletions

View File

@ -12,7 +12,6 @@ define PROJECT_ENV
{cors_allow_origins, []},
{cors_max_age, 1800},
{content_security_policy, "default-src 'self'"}
]
endef

View File

@ -361,6 +361,18 @@ end}.
]}.
%% ===========================================================================
%% Authorization
{mapping, "management.enable_uaa", "rabbitmq_management.enable_uaa",
[{datatype, {enum, [true, false]}}]}.
{mapping, "management.uaa_client_id", "rabbitmq_management.uaa_client_id",
[{datatype, string}]}.
{mapping, "management.uaa_location", "rabbitmq_management.uaa_location",
[{datatype, string}]}.
%% ===========================================================================
%% One of 'basic', 'detailed' or 'none'. See

View File

@ -975,6 +975,17 @@ or:
<pre>curl -4u 'guest:guest' -H 'content-type:application/json' -X PUT localhost:15672/api/vhost-limits/my-vhost/max-connections -d '{"value": 50}'</pre>
</td>
</tr>
<tr>
<td>X</td>
<td></td>
<td></td>
<td></td>
<td class="path">/api/auth</td>
<td>
Details about the OAuth2 configuration. It will return HTTP
status 200 with body: <pre>{"enable_uaa":"boolean", "uaa_client_id":"string", "uaa_location":"string"}</pre>
</td>
</tr>
</table>

View File

@ -16,15 +16,52 @@
<script src="js/prefs.js" type="text/javascript"></script>
<script src="js/formatters.js" type="text/javascript"></script>
<script src="js/charts.js" type="text/javascript"></script>
<script src="js/singular/singular.js" type="application/javascript"></script>
<link href="css/main.css" rel="stylesheet" type="text/css"/>
<link href="favicon.ico" rel="shortcut icon" type="image/x-icon"/>
<script type="application/javascript">
var uaa_logged_in = false;
var uaa_invalid = false;
var auth = JSON.parse(sync_get('/auth'));
enable_uaa = auth.enable_uaa;
uaa_client_id = auth.uaa_client_id;
uaa_location = auth.uaa_location;
if (enable_uaa) {
Singular.init({
singularLocation: './js/singular/',
uaaLocation: uaa_location,
clientId: uaa_client_id,
onIdentityChange: function (identity) {
uaa_logged_in = true;
start_app_login();
},
onLogout: function () {
uaa_logged_in = false;
var hash = window.location.hash.substring(1);
var params = {}
hash.split('&').map(hk => {
let temp = hk.split('=');
params[temp[0]] = temp[1]
});
if (params.error) {
uaa_invalid = true;
replace_content('login-status', '<p class="warning">' + decodeURIComponent(params.error) + ':' + decodeURIComponent(params.error_description) + '</p> <button id="loginWindow" onclick="uaa_login_window()">Click here to log out</button>');
} else {
replace_content('login-status', '<button id="loginWindow" onclick="uaa_login_window()">Click here to log in</button>');
}
}
});
}
</script>
<!--[if lte IE 8]>
<script src="js/excanvas.min.js" type="text/javascript"></script>
<link href="css/evil.css" rel="stylesheet" type="text/css"/>
<![endif]-->
</head>
<body>
<div id="outer"></div>
<div id="debug"></div>

View File

@ -261,11 +261,23 @@ dispatcher_add(function(sammy) {
});
sammy.put('#/logout', function() {
// clear a local storage value used by earlier versions
clear_pref('auth');
clear_cookie_value('auth');
location.reload();
});
// clear a local storage value used by earlier versions
clear_pref('auth');
clear_cookie_value('auth');
if (uaa_logged_in) {
clear_pref('uaa_token');
var redirect;
if (window.location.hash != "") {
redirect = window.location.href.split(window.location.hash)[0];
} else {
redirect = window.location.href
};
uaa_logged_in = false;
var logoutRedirectUrl = Singular.properties.uaaLocation + '/logout.do?client_id=' + Singular.properties.clientId + '&redirect=' + redirect;
get(logoutRedirectUrl, "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", function(req) { });
}
location.reload();
});
sammy.put('#/rate-options', function() {
update_rate_options(this);

View File

@ -764,3 +764,7 @@ var chart_data = {};
// whenever a UI requests a page that doesn't exist
// because things were deleted between refreshes
var last_page_out_of_range_error = 0;
var enable_uaa;
var uaa_client_id;
var uaa_location;

View File

@ -1,6 +1,17 @@
$(document).ready(function() {
replace_content('outer', format('login', {}));
start_app_login();
if (enable_uaa) {
get(uaa_location + "/info", "application/json", function(req) {
if (req.status !== 200) {
replace_content('outer', format('login_uaa', {}));
replace_content('login-status', '<p class="warning">' + uaa_location + " does not appear to be a running UAA instance or may not have a trusted SSL certificate" + '</p> <button id="loginWindow" onclick="uaa_login_window()">Single Sign On</button>');
} else {
replace_content('outer', format('login_uaa', {}));
}
});
} else {
replace_content('outer', format('login', {}));
start_app_login();
}
});
function dispatcher_add(fun) {
@ -58,6 +69,15 @@ function login_route_with_path() {
window.location.replace(location);
}
function getParameterByName(name) {
var match = RegExp('[#&]' + name + '=([^&]*)').exec(window.location.hash);
return match && decodeURIComponent(match[1].replace(/\+/g, ' '));
}
function getAccessToken() {
return getParameterByName('access_token');
}
function start_app_login() {
app = new Sammy.Application(function () {
this.get('#/', function() {});
@ -70,19 +90,58 @@ function start_app_login() {
this.get('#/login/:username/:password', login_route);
this.get(/\#\/login\/(.*)/, login_route_with_path);
});
app.run();
if (get_cookie_value('auth') != null) {
check_login();
if (enable_uaa) {
var token = getAccessToken();
if (token != null) {
set_auth_pref(uaa_client_id + ':' + token);
store_pref('uaa_token', token);
check_login();
} else if(has_auth_cookie_value()) {
check_login();
};
} else {
app.run();
if (get_cookie_value('auth') != null) {
check_login();
}
}
}
function uaa_logout_window() {
uaa_invalid = true;
uaa_login_window();
}
function uaa_login_window() {
var redirect;
if (window.location.hash != "") {
redirect = window.location.href.split(window.location.hash)[0];
} else {
redirect = window.location.href
};
var loginRedirectUrl;
if (uaa_invalid) {
loginRedirectUrl = Singular.properties.uaaLocation + '/logout.do?client_id=' + Singular.properties.clientId + '&redirect=' + redirect;
} else {
loginRedirectUrl = Singular.properties.uaaLocation + '/oauth/authorize?response_type=token&client_id=' + Singular.properties.clientId + '&redirect_uri=' + redirect;
};
window.open(loginRedirectUrl, "LOGIN_WINDOW");
}
function check_login() {
user = JSON.parse(sync_get('/whoami'));
if (user == false) {
// clear a local storage value used by earlier versions
clear_pref('auth');
clear_pref('uaa_token');
clear_cookie_value('auth');
replace_content('login-status', '<p>Login failed</p>');
if (enable_uaa) {
uaa_invalid = true;
replace_content('login-status', '<button id="loginWindow" onclick="uaa_login_window()">Log out</button>');
} else {
replace_content('login-status', '<p>Login failed</p>');
}
}
else {
hide_popup_warn();
@ -138,12 +197,20 @@ function start_app() {
// just leave the history here.
//Sammy.HashLocationProxy._interval = null;
app = new Sammy.Application(dispatcher);
app.run();
var url = this.location.toString();
var hash = this.location.hash;
var pathname = this.location.pathname;
if (url.indexOf('#') == -1) {
this.location = url + '#/';
} else if (hash.indexOf('#token_type') != - 1 && pathname == '/') {
// This is equivalent to previous `if` clause when uaa authorisation is used.
// Tokens are passed in the url hash, so the url always contains a #.
// We need to check the current path is `/` and token is present,
// so we can redirect to `/#/`
this.location = url.replace(/#token_type.+/gi, "#/");
}
}
@ -560,7 +627,11 @@ function submit_import(form) {
vhost_part = '/' + esc(vhost_name);
}
var form_action = "/definitions" + vhost_part + '?auth=' + get_cookie_value('auth');
if (enable_uaa) {
var form_action = "/definitions" + vhost_part + '?token=' + get_pref('uaa_token');
} else {
var form_action = "/definitions" + vhost_part + '?auth=' + get_cookie_value('auth');
};
var fd = new FormData();
fd.append('file', file);
with_req('POST', form_action, fd, function(resp) {
@ -600,9 +671,15 @@ function postprocess() {
$('#download-definitions').on('click', function() {
var idx = $("select[name='vhost-download'] option:selected").index();
var vhost = ((idx <=0 ) ? "" : "/" + esc($("select[name='vhost-download'] option:selected").val()));
if (enable_uaa) {
var path = 'api/definitions' + vhost + '?download=' +
esc($('#download-filename').val()) +
'&auth=' + get_cookie_value('auth');
'&token=' + get_pref('uaa_token');
} else {
var path = 'api/definitions' + vhost + '?download=' +
esc($('#download-filename').val()) +
'&auth=' + get_cookie_value('auth');
};
window.location = path;
setTimeout('app.run()');
return false;
@ -1114,10 +1191,14 @@ function has_auth_cookie_value() {
}
function auth_header() {
if(has_auth_cookie_value()) {
return "Basic " + decodeURIComponent(get_cookie_value('auth'));
if(has_auth_cookie_value() && enable_uaa) {
return "Bearer " + decodeURIComponent(get_pref('uaa_token'));
} else {
return null;
if(has_auth_cookie_value()) {
return "Basic " + decodeURIComponent(get_cookie_value('auth'));
} else {
return null;
}
}
}
@ -1149,6 +1230,19 @@ function with_req(method, path, body, fun) {
req.send(body);
}
function get(url, accept, callback) {
var req = new XMLHttpRequest();
req.open("GET", url);
req.setRequestHeader("Accept", accept);
req.send();
req.onreadystatechange = function() {
if (req.readyState == XMLHttpRequest.DONE) {
callback(req);
}
};
}
function sync_get(path) {
return sync_req('GET', [], path);
}

View File

@ -0,0 +1,21 @@
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<iframe style="display: none;" id="authorizeFrame" src="about:blank"></iframe>
</body>
<script type="application/javascript" src="./rpFrame.js"></script>
<script type="application/javascript">
var authorizeWindow = document.getElementById('authorizeFrame').contentWindow;
var rpFrame = RPFrame(window.parent.Singular, authorizeWindow, window);
window.addEventListener('message', rpFrame.receiveMessage, false);
rpFrame.checkClientConfig();
rpFrame.startCheckingSession();
window.fetchAccessToken = rpFrame.fetchAccessToken;
window.afterAccess = rpFrame.afterAccess;
window.afterAuthorize = rpFrame.afterAuthorize;
</script>
</html>

View File

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<script type="application/javascript">
window.parent.afterAccess(parseInt(window.location.search.substring(1)));
</script>
</head>
<body>
</body>
</html>

View File

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<script type="application/javascript">
window.parent.afterAuthorize();
</script>
</head>
<body>
</body>
</html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -23,9 +23,13 @@
</select>
</li>
<li id="logout">
<% if (enable_uaa) { %>
<input type="submit" id="loginWindow" onclick="uaa_logout_window()" value="Log out"/>
<% } else { %>
<form action="#/logout" method="put">
<input type="submit" value="Log out"/>
</form>
<% } %>
</li>
</ul>
<div id="logo">

View File

@ -0,0 +1,5 @@
<div id="login">
<p><img src="img/rabbitmqlogo.png" alt="RabbitMQ logo" width="204" height="37"/></p>
<div id="login-status"></div>
</div>

29
deps/rabbitmq_management/scripts/seed.sh vendored Executable file
View File

@ -0,0 +1,29 @@
#!/usr/bin/env sh
# use admin context to add user and client
uaac token client get admin -s adminsecret
uaac user add rabbit_user -p rabbit_password --email rabbit_user@example.com
# these groups will end up in the scope of the users
uaac group add "rabbitmq.read:*/*"
uaac group add "rabbitmq.write:*/*"
uaac group add "rabbitmq.configure:*/*"
uaac group add "rabbitmq.tag:management"
uaac group add "rabbitmq.tag:administrator"
uaac member add "rabbitmq.read:*/*" rabbit_user
uaac member add "rabbitmq.write:*/*" rabbit_user
uaac member add "rabbitmq.configure:*/*" rabbit_user
uaac member add "rabbitmq.tag:management" rabbit_user
uaac member add "rabbitmq.tag:administrator" rabbit_user
# add the client for the management plugin. It has the implicit grant type.
# add e.g. --access_token_validity 60 --refresh_token_validity 3600 to experiment with token validity
uaac client add rabbit_user_client \
--name rabbit_user_client \
--secret '' \
--scope 'rabbitmq.* openid' \
--authorized_grant_types implicit \
--autoapprove true \
--redirect_uri 'http://localhost:15672/**'

View File

@ -172,5 +172,6 @@ dispatcher() ->
{"/healthchecks/node", rabbit_mgmt_wm_healthchecks, []},
{"/healthchecks/node/:node", rabbit_mgmt_wm_healthchecks, []},
{"/reset", rabbit_mgmt_wm_reset, []},
{"/reset/:node", rabbit_mgmt_wm_reset, []}
{"/reset/:node", rabbit_mgmt_wm_reset, []},
{"/auth", rabbit_mgmt_wm_auth, []}
].

View File

@ -19,7 +19,7 @@
%% TODO sort all this out; maybe there's scope for rabbit_mgmt_request?
-export([is_authorized/2, is_authorized_admin/2, is_authorized_admin/4,
vhost/1, vhost_from_headers/1]).
is_authorized_admin/3, vhost/1, vhost_from_headers/1]).
-export([is_authorized_vhost/2, is_authorized_user/3,
is_authorized_monitor/2, is_authorized_policies/2,
is_authorized_vhost_visible/2,
@ -85,6 +85,13 @@ is_authorized_admin(ReqData, Context) ->
<<"Not administrator user">>,
fun(#user{tags = Tags}) -> is_admin(Tags) end).
is_authorized_admin(ReqData, Context, Token) ->
is_authorized(ReqData, Context,
rabbit_data_coercion:to_binary(
application:get_env(rabbitmq_management, uaa_client_id, "")),
Token, <<"Not administrator user">>,
fun(#user{tags = Tags}) -> is_admin(Tags) end).
is_authorized_admin(ReqData, Context, Username, Password) ->
is_authorized(ReqData, Context, Username, Password,
<<"Not administrator user">>,
@ -183,6 +190,10 @@ is_authorized1(ReqData, Context, ErrorMsg, Fun) ->
is_authorized(ReqData, Context,
Username, Password,
ErrorMsg, Fun);
{bearer, Token} ->
Username = rabbit_data_coercion:to_binary(
application:get_env(rabbitmq_management, uaa_client_id, "")),
is_authorized(ReqData, Context, Username, Token, ErrorMsg, Fun);
_ ->
{{false, ?AUTH_REALM}, ReqData, Context}
end.

View File

@ -0,0 +1,60 @@
%% The contents of this file are subject to the Mozilla Public License
%% Version 1.1 (the "License"); you may not use this file except in
%% compliance with the License. You may obtain a copy of the License at
%% https://www.mozilla.org/MPL/
%%
%% Software distributed under the License is distributed on an "AS IS"
%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
%% License for the specific language governing rights and limitations
%% under the License.
%%
%% The Original Code is RabbitMQ Management Plugin.
%%
%% The Initial Developer of the Original Code is GoPivotal, Inc.
%% Copyright (c) 2007-2019 Pivotal Software, Inc. All rights reserved.
%%
-module(rabbit_mgmt_wm_auth).
-export([init/2, to_json/2, content_types_provided/2, is_authorized/2]).
-export([variances/2]).
-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_records.hrl").
-include_lib("rabbit_common/include/rabbit.hrl").
%%--------------------------------------------------------------------
init(Req, _State) ->
{cowboy_rest, rabbit_mgmt_headers:set_common_permission_headers(Req, ?MODULE), #context{}}.
variances(Req, Context) ->
{[<<"accept-encoding">>, <<"origin">>], Req, Context}.
content_types_provided(ReqData, Context) ->
{rabbit_mgmt_util:responder_map(to_json), ReqData, Context}.
to_json(ReqData, Context) ->
EnableUAA = application:get_env(rabbitmq_management, enable_uaa, false),
Data = case EnableUAA of
true ->
UAAClientId = application:get_env(rabbitmq_management, uaa_client_id, ""),
UAALocation = application:get_env(rabbitmq_management, uaa_location, ""),
case is_invalid([UAAClientId, UAALocation]) of
true ->
rabbit_log:warning("Disabling OAuth 2 authorization, relevant configuration settings are missing", []),
[{enable_uaa, false}, {uaa_client_id, <<>>}, {uaa_location, <<>>}];
false ->
[{enable_uaa, true},
{uaa_client_id, rabbit_data_coercion:to_binary(UAAClientId)},
{uaa_location, rabbit_data_coercion:to_binary(UAALocation)}]
end;
false ->
[{enable_uaa, false}, {uaa_client_id, <<>>}, {uaa_location, <<>>}]
end,
rabbit_mgmt_util:reply(Data, ReqData, Context).
is_authorized(ReqData, Context) ->
{true, ReqData, Context}.
is_invalid(List) ->
lists:any(fun(V) -> V == "" end, List).

View File

@ -136,8 +136,15 @@ accept_multipart(ReqData0, Context) ->
is_authorized(ReqData, Context) ->
case rabbit_mgmt_util:qs_val(<<"auth">>, ReqData) of
undefined -> rabbit_mgmt_util:is_authorized_admin(ReqData, Context);
Auth -> is_authorized_qs(ReqData, Context, Auth)
undefined ->
case rabbit_mgmt_util:qs_val(<<"token">>, ReqData) of
Token ->
rabbit_mgmt_util:is_authorized_admin(ReqData, Context, Token);
undefined ->
rabbit_mgmt_util:is_authorized_admin(ReqData, Context)
end;
Auth ->
is_authorized_qs(ReqData, Context, Auth)
end.
%% Support for the web UI - it can't add a normal "authorization"

View File

@ -146,7 +146,8 @@ all_tests() -> [
vhost_limit_set_test,
rates_test,
single_active_consumer_cq_test,
single_active_consumer_qq_test].
single_active_consumer_qq_test,
oauth_test].
%% -------------------------------------------------------------------
%% Testsuite setup/teardown.
@ -3069,6 +3070,32 @@ stats_redirect_test(Config) ->
assert_permanent_redirect(Config, "doc/stats.html", "/api/index.html"),
passed.
oauth_test(Config) ->
Map1 = http_get(Config, "/auth", ?OK),
%% Defaults
?assertEqual(false, maps:get(enable_uaa, Map1)),
?assertEqual(<<>>, maps:get(uaa_client_id, Map1)),
?assertEqual(<<>>, maps:get(uaa_location, Map1)),
%% Misconfiguration
rabbit_ct_broker_helpers:rpc(Config, 0, application, set_env,
[rabbitmq_management, enable_uaa, true]),
Map2 = http_get(Config, "/auth", ?OK),
?assertEqual(false, maps:get(enable_uaa, Map2)),
?assertEqual(<<>>, maps:get(uaa_client_id, Map2)),
?assertEqual(<<>>, maps:get(uaa_location, Map2)),
%% Valid config
rabbit_ct_broker_helpers:rpc(Config, 0, application, set_env,
[rabbitmq_management, uaa_client_id, "rabbit_user"]),
rabbit_ct_broker_helpers:rpc(Config, 0, application, set_env,
[rabbitmq_management, uaa_location, "http://localhost:8080/uaa"]),
Map3 = http_get(Config, "/auth", ?OK),
?assertEqual(true, maps:get(enable_uaa, Map3)),
?assertEqual(<<"rabbit_user">>, maps:get(uaa_client_id, Map3)),
?assertEqual(<<"http://localhost:8080/uaa">>, maps:get(uaa_location, Map3)),
%% cleanup
rabbit_ct_broker_helpers:rpc(Config, 0, application, unset_env,
[rabbitmq_management, enable_uaa]).
%% -------------------------------------------------------------------
%% Helpers.
%% -------------------------------------------------------------------