Merge pull request #11448 from rabbitmq/dynamic-load-oauth-deps-ui

Dynamically load oauth-related libraries
This commit is contained in:
Michael Klishin 2024-06-20 21:37:51 -04:00 committed by GitHub
commit c67c940bcc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 503 additions and 230 deletions

View File

@ -16,48 +16,17 @@
<script src="js/prefs.js" type="text/javascript"></script> <script src="js/prefs.js" type="text/javascript"></script>
<script src="js/formatters.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/charts.js" type="text/javascript"></script>
<script src="js/oidc-oauth/helper.js"></script> <script src="js/oidc-oauth/bootstrap.js" type="module"></script>
<script src="js/oidc-oauth/oidc-client-ts.js" type="text/javascript"></script>
<script src="js/oidc-oauth/bootstrap.js"></script>
<link href="css/main.css" rel="stylesheet" type="text/css"/> <link href="css/main.css" rel="stylesheet" type="text/css"/>
<link href="favicon.ico" rel="shortcut icon" type="image/x-icon"/> <link href="favicon.ico" rel="shortcut icon" type="image/x-icon"/>
<script type="application/javascript"> <script type="module">
var oauth = oauth_initialize_if_required(); window.oauth = oauth_initialize_if_required();
if (oauth.enabled) {
if (!oauth.sp_initiated) {
oauth.logged_in = has_auth_credentials();
oauth.access_token = get_auth_credentials(); // DEPRECATED
} else {
oauth_is_logged_in().then( status => {
if (status.loggedIn && !has_auth_credentials()) {
oauth.logged_in = false;
oauth_initiateLogout();
} else {
if (!status.loggedIn) {
clear_auth();
} else {
oauth.logged_in = true;
oauth.access_token = status.user.access_token; // DEPRECATED
oauth.expiryDate = new Date(status.user.expires_at * 1000); // it is epoch in seconds
let current = new Date();
_management_logger.debug('token expires in ', (oauth.expiryDate-current)/1000,
'secs at : ', oauth.expiryDate );
oauth.user_name = status.user.profile['user_name'];
if (!oauth.user_name || oauth.user_name == '') {
oauth.user_name = status.user.profile['sub'];
}
oauth.scopes = status.user.scope;
}
}
});
}
}
</script> </script>
<!--[if lte IE 8]> <!--[if lte IE 8]>
<script src="js/excanvas.min.js" type="text/javascript"></script> <script src="js/excanvas.min.js" type="text/javascript"></script>
<link href="css/evil.css" rel="stylesheet" type="text/css"/> <link href="css/evil.css" rel="stylesheet" type="text/css"/>

View File

@ -698,6 +698,7 @@ function DisplayControl() {
} }
// Set up the above vars // Set up the above vars
function setup_global_vars(overview) { function setup_global_vars(overview) {
rates_mode = overview.rates_mode; rates_mode = overview.rates_mode;
@ -715,7 +716,7 @@ function setup_global_vars(overview) {
user_name = fmt_escape_html(user.name); user_name = fmt_escape_html(user.name);
$('#header #logout').prepend( $('#header #logout').prepend(
'User ' + (user_administrator && !oauth.enabled ? '<a href="#/users/' + user_name + '">' + user_name + '</a>' : user_name) 'User ' + (user_administrator && user.is_internal_user ? '<a href="#/users/' + user_name + '">' + user_name + '</a>' : user_name)
); );
var product = overview.rabbitmq_version; var product = overview.rabbitmq_version;

View File

@ -1,17 +1,19 @@
$(document).ready(function() { $(document).ready(function() {
var url_string = window.location.href; var url_string = window.location.href;
var url = new URL(url_string); var url = new URL(url_string);
var error = url.searchParams.get('error'); var error = url.searchParams.get('error');
if (error) { if (error) {
renderWarningMessageInLoginStatus(fmt_escape_html(error)); if (oauth.enabled) {
} else { renderWarningMessageInLoginStatus(oauth, fmt_escape_html(error));
if (oauth.enabled) {
startWithOAuthLogin();
} else {
startWithLoginPage();
}
} }
} else {
if (oauth.enabled) {
startWithOAuthLogin(oauth);
} else {
startWithLoginPage();
}
}
}); });
function startWithLoginPage() { function startWithLoginPage() {
@ -27,85 +29,18 @@ function removeDuplicates(array){
} }
return output return output
} }
function warningMessageOAuthResource(oauthResource, reason) {
return "OAuth resource [<b>" + (oauthResource["label"] != null ? oauthResource.label : oauthResource.id) +
"</b>] not available. OpenId Discovery endpoint " + readiness_url(oauthResource) + reason
}
function warningMessageOAuthResources(commonProviderURL, oauthResources, reason) {
return "OAuth resources [ <b>" + oauthResources.map(resource => resource["label"] != null ? resource.label : resource.id).join("</b>,<b>")
+ "</b>] not available. OpenId Discovery endpoint " + commonProviderURL + reason
}
function startWithOAuthLogin () {
function startWithOAuthLogin (oauth) {
store_pref("oauth-return-to", window.location.hash); store_pref("oauth-return-to", window.location.hash);
if (!oauth.logged_in) { if (!oauth.logged_in) {
hasAnyResourceServerReady(oauth, (oauth, warnings) => { render_login_oauth(oauth, warnings); start_app_login(); })
// Find out how many distinct oauthServers are configured
let oauthServers = removeDuplicates(oauth.resource_servers.filter((resource) => resource.sp_initiated))
oauthServers.forEach(function(entry) { console.log(readiness_url(entry)) })
if (oauthServers.length > 0) { // some resources are sp_initiated but there could be idp_initiated too
Promise.allSettled(oauthServers.map(oauthServer => fetch(readiness_url(oauthServer)).then(res => res.json())))
.then(results => {
results.forEach(function(entry) { console.log(entry) })
let notReadyServers = []
let notCompliantServers = []
for (let i = 0; i < results.length; i++) {
switch (results[i].status) {
case "fulfilled":
try {
validate_openid_configuration(results[i].value)
}catch(e) {
console.log("Unable to connect to " + oauthServers[i].oauth_provider_url + ". " + e)
notCompliantServers.push(oauthServers[i].oauth_provider_url)
}
break
case "rejected":
notReadyServers.push(oauthServers[i].oauth_provider_url)
break
}
}
const spOauthServers = oauth.resource_servers.filter((resource) => resource.sp_initiated)
const groupByProviderURL = spOauthServers.reduce((group, oauthServer) => {
const { oauth_provider_url } = oauthServer;
group[oauth_provider_url] = group[oauth_provider_url] ?? [];
group[oauth_provider_url].push(oauthServer);
return group;
}, {})
let warnings = []
for(var url in groupByProviderURL){
console.log(url + ': ' + groupByProviderURL[url]);
const notReadyResources = groupByProviderURL[url].filter((oauthserver) => notReadyServers.includes(oauthserver.oauth_provider_url))
const notCompliantResources = groupByProviderURL[url].filter((oauthserver) => notCompliantServers.includes(oauthserver.oauth_provider_url))
if (notReadyResources.length == 1) {
warnings.push(warningMessageOAuthResource(notReadyResources[0], " not reachable"))
}else if (notReadyResources.length > 1) {
warnings.push(warningMessageOAuthResources(url, notReadyResources, " not reachable"))
}
if (notCompliantResources.length == 1) {
warnings.push(warningMessageOAuthResource(notCompliantResources[0], " not compliant"))
}else if (notCompliantResources.length > 1) {
warnings.push(warningMessageOAuthResources(url, notCompliantResources, " not compliant"))
}
}
console.log("warnings:" + warnings)
oauth.declared_resource_servers_count = oauth.resource_servers.length
oauth.resource_servers = oauth.resource_servers.filter((resource) =>
!notReadyServers.includes(resource.oauth_provider_url) && !notCompliantServers.includes(resource.oauth_provider_url))
render_login_oauth(warnings)
start_app_login()
})
}else { // there are only idp_initiated resources
render_login_oauth()
start_app_login()
}
} else { } else {
start_app_login() start_app_login()
} }
} }
function render_login_oauth(messages) { function render_login_oauth(oauth, messages) {
let formatData = {} let formatData = {}
formatData.warnings = [] formatData.warnings = []
formatData.notAuthorized = false formatData.notAuthorized = false
@ -118,7 +53,6 @@ function render_login_oauth(messages) {
} else if (typeof messages == "string") { } else if (typeof messages == "string") {
formatData.warnings = [messages] formatData.warnings = [messages]
formatData.notAuthorized = messages == "Not authorized" formatData.notAuthorized = messages == "Not authorized"
console.log("Single error message")
} }
replace_content('outer', format('login_oauth', formatData)) replace_content('outer', format('login_oauth', formatData))
@ -127,13 +61,11 @@ function render_login_oauth(messages) {
$('#login').on('click', 'div.section h2, div.section-hidden h2', function() { $('#login').on('click', 'div.section h2, div.section-hidden h2', function() {
toggle_visibility($(this)); toggle_visibility($(this));
}); });
} }
function renderWarningMessageInLoginStatus(message) { function renderWarningMessageInLoginStatus(oauth, message) {
render_login_oauth(message) render_login_oauth(oauth, message)
} }
function dispatcher_add(fun) { function dispatcher_add(fun) {
dispatcher_modules.push(fun); dispatcher_modules.push(fun);
if (dispatcher_modules.length == extension_count) { if (dispatcher_modules.length == extension_count) {
@ -187,9 +119,10 @@ function check_login () {
if (user == false || user.error) { if (user == false || user.error) {
clear_auth(); clear_auth();
if (oauth.enabled) { if (oauth.enabled) {
hide_popup_warn(); //hide_popup_warn();
renderWarningMessageInLoginStatus('Not authorized'); renderWarningMessageInLoginStatus(oauth, 'Not authorized');
} else { } else {
//hide_popup_warn();
replace_content('login-status', '<p>Login failed</p>'); replace_content('login-status', '<p>Login failed</p>');
} }
return false; return false;
@ -323,6 +256,7 @@ function dynamic_load(filename) {
element.setAttribute('type', 'text/javascript'); element.setAttribute('type', 'text/javascript');
element.setAttribute('src', 'js/' + filename); element.setAttribute('src', 'js/' + filename);
document.getElementsByTagName('head')[0].appendChild(element); document.getElementsByTagName('head')[0].appendChild(element);
return element;
} }
function update_interval() { function update_interval() {
@ -350,6 +284,10 @@ function update_interval() {
function go_to(url) { function go_to(url) {
this.location = url; this.location = url;
} }
function go_to_home() {
// location.href = rabbit_path_prefix() + "/"
location.href = "/"
}
function set_timer_interval(interval) { function set_timer_interval(interval) {
timer_interval = interval; timer_interval = interval;
@ -1472,16 +1410,16 @@ function sync_req(type, params0, path_template, options) {
else else
// rabbitmq/rabbitmq-management#732 // rabbitmq/rabbitmq-management#732
// https://developer.mozilla.org/en-US/docs/Glossary/Truthy // https://developer.mozilla.org/en-US/docs/Glossary/Truthy
return {result: true, http_status: req.status, req_params: params}; return {result: true, http_status: req.status, req_params: params, responseText: req.responseText};
} }
else { else {
return false; return false;
} }
} }
function initiate_logout(error = "") { function initiate_logout(oauth, error = "") {
clear_pref('auth'); clear_pref('auth');
clear_cookie_value('auth'); clear_cookie_value('auth');
renderWarningMessageInLoginStatus(error); renderWarningMessageInLoginStatus(oauth, error);
} }
function check_bad_response(req, full_page_404) { function check_bad_response(req, full_page_404) {
// 1223 == 204 - see https://www.enhanceie.com/ie/bugs.asp // 1223 == 204 - see https://www.enhanceie.com/ie/bugs.asp
@ -1502,7 +1440,7 @@ function check_bad_response(req, full_page_404) {
if (error == 'bad_request' || error == 'not_found' || error == 'not_authorised' || error == 'not_authorized') { if (error == 'bad_request' || error == 'not_found' || error == 'not_authorised' || error == 'not_authorized') {
if ((req.status == 401 || req.status == 403) && oauth.enabled) { if ((req.status == 401 || req.status == 403) && oauth.enabled) {
initiate_logout(reason); initiate_logout(oauth, reason);
} else { } else {
show_popup('warn', fmt_escape_html(reason)); show_popup('warn', fmt_escape_html(reason));
} }

View File

@ -1,3 +1,4 @@
import {oidc} from './oidc-client-ts.js';
var mgr; var mgr;
var _management_logger; var _management_logger;
@ -85,9 +86,61 @@ function auth_settings_apply_defaults(authSettings) {
return authSettings; return authSettings;
} }
var oauth_settings = { oauth_enabled : false}
export function set_oauth_settings(settings) {
oauth_settings = settings
}
function get_oauth_settings() {
return oauth_settings
}
export function oauth_initialize_if_required(state = "index") {
let oauth = oauth_initialize(get_oauth_settings())
if (!oauth.enabled) return oauth;
switch (state) {
case 'login-callback':
oauth_completeLogin(); break;
case 'logout-callback':
oauth_completeLogout(); break;
default:
oauth = oauth_initiate(oauth);
}
return oauth;
}
export function oauth_initiate(oauth) {
if (oauth.enabled) {
if (!oauth.sp_initiated) {
oauth.logged_in = has_auth_credentials();
} else {
oauth_is_logged_in().then( status => {
if (status.loggedIn && !has_auth_credentials()) {
oauth.logged_in = false;
oauth_initiateLogout();
} else {
if (!status.loggedIn) {
clear_auth();
} else {
oauth.logged_in = true;
oauth.expiryDate = new Date(status.user.expires_at * 1000); // it is epoch in seconds
let current = new Date();
_management_logger.debug('token expires in ', (oauth.expiryDate-current)/1000,
'secs at : ', oauth.expiryDate );
oauth.user_name = status.user.profile['user_name'];
if (!oauth.user_name || oauth.user_name == '') {
oauth.user_name = status.user.profile['sub'];
}
oauth.scopes = status.user.scope;
}
}
});
}
}
return oauth;
}
function oauth_initialize_user_manager(resource_server) { function oauth_initialize_user_manager(resource_server) {
oidcSettings = { let oidcSettings = {
userStore: new oidc.WebStorageStateStore({ store: window.localStorage }), userStore: new oidc.WebStorageStateStore({ store: window.localStorage }),
authority: resource_server.oauth_provider_url, authority: resource_server.oauth_provider_url,
client_id: resource_server.oauth_client_id, client_id: resource_server.oauth_client_id,
@ -119,7 +172,7 @@ function oauth_initialize_user_manager(resource_server) {
oidc.Log.setLogger(console); oidc.Log.setLogger(console);
mgr = new oidc.UserManager(oidcSettings); mgr = new oidc.UserManager(oidcSettings);
oauth.readiness_url = mgr.settings.metadataUrl; // oauth.readiness_url = mgr.settings.metadataUrl;
_management_logger = new oidc.Logger("Management"); _management_logger = new oidc.Logger("Management");
@ -133,15 +186,13 @@ function oauth_initialize_user_manager(resource_server) {
_management_logger.error("token expiring failed due to ", err); _management_logger.error("token expiring failed due to ", err);
}); });
mgr.events.addUserLoaded(function(user) { mgr.events.addUserLoaded(function(user) {
console.log("addUserLoaded setting oauth.access_token ") set_token_auth(user.access_token)
oauth.access_token = user.access_token // DEPRECATED
set_token_auth(oauth.access_token)
}); });
} }
function oauth_initialize(authSettings) { export function oauth_initialize(authSettings) {
authSettings = auth_settings_apply_defaults(authSettings); authSettings = auth_settings_apply_defaults(authSettings);
oauth = { let oauth = {
"logged_in": false, "logged_in": false,
"enabled" : authSettings.oauth_enabled, "enabled" : authSettings.oauth_enabled,
"resource_servers" : authSettings.resource_servers, "resource_servers" : authSettings.resource_servers,
@ -149,12 +200,12 @@ function oauth_initialize(authSettings) {
} }
if (!oauth.enabled) return oauth; if (!oauth.enabled) return oauth;
resource_server = null let resource_server = null;
if (oauth.resource_servers.length == 1) { if (oauth.resource_servers.length == 1) {
resource_server = oauth.resource_servers[0] resource_server = oauth.resource_servers[0]
} else if (has_auth_resource()) { } else if (has_auth_resource()) {
resource_server = lookup_resource_server(get_auth_resource()) resource_server = lookup_resource_server(get_auth_resource(), oauth.resource_servers)
} }
if (resource_server) { if (resource_server) {
@ -189,18 +240,18 @@ function oauth_is_logged_in() {
return { "user": user, "loggedIn": !user.expired }; return { "user": user, "loggedIn": !user.expired };
}); });
} }
function lookup_resource_server(resource_server_id) { function lookup_resource_server(resource_server_id, resource_servers) {
let i = 0; let i = 0;
while (i < oauth.resource_servers.length && oauth.resource_servers[i].id != resource_server_id) { while (i < resource_servers.length && resource_servers[i].id != resource_server_id) {
i++; i++;
} }
if (i < oauth.resource_servers.length) return oauth.resource_servers[i] if (i < resource_servers.length) return resource_servers[i]
else return null else return null
} }
function oauth_initiateLogin(resource_server_id) { export function oauth_initiateLogin(resource_server_id) {
resource_server = lookup_resource_server(resource_server_id) let resource_server = lookup_resource_server(resource_server_id, oauth.resource_servers)
if (!resource_server) return; if (!resource_server) return;
set_auth_resource(resource_server_id) set_auth_resource(resource_server_id)
@ -220,19 +271,15 @@ function oauth_initiateLogin(resource_server_id) {
} }
} }
function oauth_redirectToHome(oauth) { function oauth_redirectToHome() {
set_token_auth(oauth.access_token) let path = get_pref("oauth-return-to")
path = get_pref("oauth-return-to");
clear_pref("oauth-return-to") clear_pref("oauth-return-to")
go_to(path) go_to( !path ? "" : path)
} }
function go_to(path) { function go_to(path) {
location.href = rabbit_path_prefix() + "/" + path location.href = rabbit_path_prefix() + "/" + path
} }
function go_to_home() {
location.href = rabbit_path_prefix() + "/"
}
function go_to_authority() { function go_to_authority() {
location.href = oauth.authority location.href = oauth.authority
} }
@ -242,14 +289,17 @@ function oauth_redirectToLogin(error) {
location.href = rabbit_path_prefix() + "/?error=" + error location.href = rabbit_path_prefix() + "/?error=" + error
} }
} }
function oauth_completeLogin() { export function oauth_completeLogin() {
mgr.signinRedirectCallback().then(user => oauth_redirectToHome(user)).catch(function(err) { mgr.signinRedirectCallback().then(function(user) {
_management_logger.error(err) set_token_auth(user.access_token);
oauth_redirectToLogin(err) oauth_redirectToHome();
}).catch(function(err) {
_management_logger.error(err)
oauth_redirectToLogin(err)
}); });
} }
function oauth_initiateLogout() { export function oauth_initiateLogout() {
if (oauth.sp_initiated) { if (oauth.sp_initiated) {
mgr.metadataService.getEndSessionEndpoint().then(endpoint => { mgr.metadataService.getEndSessionEndpoint().then(endpoint => {
if (endpoint == undefined) { if (endpoint == undefined) {
@ -268,7 +318,7 @@ function oauth_initiateLogout() {
} }
} }
function oauth_completeLogout() { export function oauth_completeLogout() {
clear_auth() clear_auth()
mgr.signoutRedirectCallback().then(_ => oauth_redirectToLogin()) mgr.signoutRedirectCallback().then(_ => oauth_redirectToLogin())
} }
@ -287,3 +337,74 @@ function validate_openid_configuration(payload) {
} }
} }
function warningMessageOAuthResource(oauthResource, reason) {
return "OAuth resource [<b>" + (oauthResource["label"] != null ? oauthResource.label : oauthResource.id) +
"</b>] not available. OpenId Discovery endpoint " + readiness_url(oauthResource) + reason
}
function warningMessageOAuthResources(commonProviderURL, oauthResources, reason) {
return "OAuth resources [ <b>" + oauthResources.map(resource => resource["label"] != null ? resource.label : resource.id).join("</b>,<b>")
+ "</b>] not available. OpenId Discovery endpoint " + commonProviderURL + reason
}
export function hasAnyResourceServerReady(oauth, onReadyCallback) {
// Find out how many distinct oauthServers are configured
let oauthServers = removeDuplicates(oauth.resource_servers.filter((resource) => resource.sp_initiated))
oauthServers.forEach(function(entry) { console.log(readiness_url(entry)) })
if (oauthServers.length > 0) { // some resources are sp_initiated but there could be idp_initiated too
Promise.allSettled(oauthServers.map(oauthServer => fetch(readiness_url(oauthServer)).then(res => res.json())))
.then(results => {
results.forEach(function(entry) { console.log(entry) })
let notReadyServers = []
let notCompliantServers = []
for (let i = 0; i < results.length; i++) {
switch (results[i].status) {
case "fulfilled":
try {
validate_openid_configuration(results[i].value)
}catch(e) {
console.log("Unable to connect to " + oauthServers[i].oauth_provider_url + ". " + e)
notCompliantServers.push(oauthServers[i].oauth_provider_url)
}
break
case "rejected":
notReadyServers.push(oauthServers[i].oauth_provider_url)
break
}
}
const spOauthServers = oauth.resource_servers.filter((resource) => resource.sp_initiated)
const groupByProviderURL = spOauthServers.reduce((group, oauthServer) => {
const { oauth_provider_url } = oauthServer;
group[oauth_provider_url] = group[oauth_provider_url] ?? [];
group[oauth_provider_url].push(oauthServer);
return group;
}, {})
let warnings = []
for(var url in groupByProviderURL){
console.log(url + ': ' + groupByProviderURL[url]);
const notReadyResources = groupByProviderURL[url].filter((oauthserver) => notReadyServers.includes(oauthserver.oauth_provider_url))
const notCompliantResources = groupByProviderURL[url].filter((oauthserver) => notCompliantServers.includes(oauthserver.oauth_provider_url))
if (notReadyResources.length == 1) {
warnings.push(warningMessageOAuthResource(notReadyResources[0], " not reachable"))
}else if (notReadyResources.length > 1) {
warnings.push(warningMessageOAuthResources(url, notReadyResources, " not reachable"))
}
if (notCompliantResources.length == 1) {
warnings.push(warningMessageOAuthResource(notCompliantResources[0], " not compliant"))
}else if (notCompliantResources.length > 1) {
warnings.push(warningMessageOAuthResources(url, notCompliantResources, " not compliant"))
}
}
console.log("warnings:" + warnings)
oauth.declared_resource_servers_count = oauth.resource_servers.length
oauth.resource_servers = oauth.resource_servers.filter((resource) =>
!notReadyServers.includes(resource.oauth_provider_url) && !notCompliantServers.includes(resource.oauth_provider_url))
onReadyCallback(oauth, warnings)
})
}else {
onReadyCallback(oauth, [])
}
}

View File

@ -7,12 +7,11 @@
<script src="../jquery-3.5.1.min.js"></script> <script src="../jquery-3.5.1.min.js"></script>
<script src="../base64.js" type="text/javascript"></script> <script src="../base64.js" type="text/javascript"></script>
<script src="../prefs.js" ></script> <script src="../prefs.js" ></script>
<script src="./oidc-client-ts.js" ></script> <script src="./bootstrap.js" type="module" ></script>
<script src="./helper.js"></script> <script type="module">
<script src="./bootstrap.js"></script> window.oauth = oauth_initialize_if_required("login-callback");
<script type="text/javascript">
</script>
if (oauth_initialize_if_required()) oauth_completeLogin()
</script>
</body> </body>
</html> </html>

View File

@ -4,15 +4,15 @@
</head> </head>
<body> <body>
<script src="./oidc-client-ts.js" ></script> <script src="./bootstrap.js" type="module" ></script>
<script src="./helper.js"></script> <script type="module">
<script src="./bootstrap.js"></script> window.oauth = oauth_initialize_if_required("logout-callback");
<script type="text/javascript">
if (oauth_initialize_if_required()) { </script>
oauth_completeLogout()
}
</script>
</body> </body>
</html> </html>

View File

@ -3180,3 +3180,4 @@ var oidc = (() => {
return __toCommonJS(src_exports); return __toCommonJS(src_exports);
})(); })();
//# sourceMappingURL=oidc-client-ts.js.map //# sourceMappingURL=oidc-client-ts.js.map
export {oidc};

View File

@ -47,7 +47,7 @@ start_devkeycloak() {
wait_for_oidc_endpoint devkeycloak $DEVKEYCLOAK_URL $MOUNT_DEVKEYCLOAK_CONF_DIR/ca_certificate.pem wait_for_oidc_endpoint devkeycloak $DEVKEYCLOAK_URL $MOUNT_DEVKEYCLOAK_CONF_DIR/ca_certificate.pem
end "devkeycloak is ready" end "devkeycloak is ready"
print " Note: If you modify devkeycloak configuration. Make sure to run the following command to export the configuration." print " Note: If you modify devkeycloak configuration, make sure to run the following command to export the configuration."
print " docker exec -it devkeycloak /opt/keycloak/bin/kc.sh export --users realm_file --realm test --dir /opt/keycloak/data/import/" print " docker exec -it devkeycloak /opt/keycloak/bin/kc.sh export --users realm_file --realm test --dir /opt/keycloak/data/import/"
} }

View File

@ -46,7 +46,7 @@ start_prodkeycloak() {
wait_for_oidc_endpoint prodkeycloak $PRODKEYCLOAK_URL $MOUNT_PRODKEYCLOAK_CONF_DIR/ca_certificate.pem wait_for_oidc_endpoint prodkeycloak $PRODKEYCLOAK_URL $MOUNT_PRODKEYCLOAK_CONF_DIR/ca_certificate.pem
end "prodkeycloak is ready" end "prodkeycloak is ready"
print " Note: If you modify prodkeycloak configuration. Make sure to run the following command to export the configuration." print " Note: If you modify prodkeycloak configuration, make sure to run the following command to export the configuration."
print " docker exec -it prodkeycloak /opt/keycloak/bin/kc.sh export --users realm_file --realm test --dir /opt/keycloak/data/import/" print " docker exec -it prodkeycloak /opt/keycloak/bin/kc.sh export --users realm_file --realm test --dir /opt/keycloak/data/import/"
} }

View File

@ -1,5 +1,6 @@
authnz-mgt/basic-auth-behind-proxy.sh authnz-mgt/basic-auth-behind-proxy.sh
authnz-mgt/basic-auth.sh authnz-mgt/basic-auth.sh
authnz-mgt/basic-auth-with-mgt-prefix.sh
authnz-mgt/multi-oauth-with-basic-auth-when-idps-down.sh authnz-mgt/multi-oauth-with-basic-auth-when-idps-down.sh
authnz-mgt/multi-oauth-with-basic-auth.sh authnz-mgt/multi-oauth-with-basic-auth.sh
authnz-mgt/multi-oauth-without-basic-auth-and-resource-label-and-scopes.sh authnz-mgt/multi-oauth-without-basic-auth-and-resource-label-and-scopes.sh

View File

@ -13,7 +13,7 @@
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"chromedriver": "^123.0.0", "chromedriver": "^125.0.0",
"ejs": "^3.1.8", "ejs": "^3.1.8",
"express": "^4.18.2", "express": "^4.18.2",
"geckodriver": "^3.0.2", "geckodriver": "^3.0.2",

View File

@ -0,0 +1,9 @@
#!/usr/bin/env bash
SCRIPT="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
TEST_CASES_PATH=/basic-auth
PROFILES="mgt-prefix"
source $SCRIPT/../../bin/suite_template $@
run

View File

@ -44,6 +44,13 @@
"monitoring" "monitoring"
], ],
"limits": {} "limits": {}
},
{
"name": "rabbit_no_management",
"password_hash": "Joz9zzUBOrX10lB3GisWN5oTXK+wj0gxS/nyrfTYmBOuhps5",
"hashing_algorithm": "rabbit_password_hashing_sha256",
"tags": [ ],
"limits": {}
} }
], ],
"vhosts": [ "vhosts": [
@ -65,6 +72,13 @@
"configure": ".*", "configure": ".*",
"write": ".*", "write": ".*",
"read": ".*" "read": ".*"
},
{
"user": "rabbit_no_management",
"vhost": "/",
"configure": ".*",
"write": ".*",
"read": ".*"
} }
] ]

View File

@ -1,4 +1,4 @@
auth_backends.1 = rabbit_auth_backend_internal auth_backends.1 = rabbit_auth_backend_internal
management.login_session_timeout = 150 management.login_session_timeout = 1
load_definitions = ${IMPORT_DIR}/users.json load_definitions = ${IMPORT_DIR}/users.json

View File

@ -0,0 +1 @@
management.path_prefix = /my-prefix/another-prefix

View File

@ -0,0 +1,39 @@
const { By, Key, until, Builder } = require('selenium-webdriver')
require('chromedriver')
const assert = require('assert')
const { buildDriver, goToHome, captureScreensFor, teardown, delay } = require('../utils')
const LoginPage = require('../pageobjects/LoginPage')
const OverviewPage = require('../pageobjects/OverviewPage')
describe('Once user is logged in', function () {
let homePage
let idpLogin
let overview
let captureScreen
this.timeout(65000) // hard-coded to 25secs because this test requires 35sec to run
before(async function () {
driver = buildDriver()
await goToHome(driver)
login = new LoginPage(driver)
overview = new OverviewPage(driver)
captureScreen = captureScreensFor(driver, __filename)
await login.login('guest', 'guest')
await overview.isLoaded()
})
it('it has to login after the session expires', async function () {
await delay(60000)
await login.isLoaded()
await login.login('guest', 'guest')
await overview.isLoaded()
await overview.clickOnConnectionsTab() // and we can still interact with the ui
})
after(async function () {
await teardown(driver, this, captureScreen)
})
})

View File

@ -0,0 +1,59 @@
const { By, Key, until, Builder } = require('selenium-webdriver')
require('chromedriver')
const assert = require('assert')
const { buildDriver, goToHome, captureScreensFor, teardown, delay } = require('../utils')
const LoginPage = require('../pageobjects/LoginPage')
const OverviewPage = require('../pageobjects/OverviewPage')
describe('An user without management tag', function () {
let homePage
let idpLogin
let overview
let captureScreen
before(async function () {
driver = buildDriver()
await goToHome(driver)
login = new LoginPage(driver)
overview = new OverviewPage(driver)
captureScreen = captureScreensFor(driver, __filename)
assert.ok(!await login.isPopupWarningDisplayed())
await login.login('rabbit_no_management', 'rabbit_no_management')
await !overview.isLoaded()
})
it('cannot log in into the management ui', async function () {
const visible = await login.isWarningVisible()
assert.ok(visible)
})
it('should get "Login failed" warning message', async function(){
assert.equal('Login failed', await login.getWarning())
})
it('should get popup warning dialog', async function(){
assert.ok(login.isPopupWarningDisplayed())
assert.equal('Not_Authorized', await login.getPopupWarning())
})
describe("After clicking on popup warning dialog button", function() {
before(async function () {
await login.closePopupWarning()
})
it('should close popup warning', async function(){
await delay(1000)
const visible = await login.isPopupWarningDisplayed()
assert.ok(!visible)
})
})
after(async function () {
await teardown(driver, this, captureScreen)
})
})

View File

@ -1,5 +1,5 @@
[accept,amqp10_client,amqp_client,base64url,cowboy,cowlib,eetcd,gun,jose, [accept,amqp10_client,amqp_client,base64url,cowboy,cowlib,eetcd,gun,jose,
oauth2_client,prometheus,rabbitmq_auth_backend_cache, oauth2_client,prometheus,rabbitmq_amqp1_0,rabbitmq_auth_backend_cache,
rabbitmq_auth_backend_http,rabbitmq_auth_backend_ldap, rabbitmq_auth_backend_http,rabbitmq_auth_backend_ldap,
rabbitmq_auth_backend_oauth2,rabbitmq_auth_mechanism_ssl,rabbitmq_aws, rabbitmq_auth_backend_oauth2,rabbitmq_auth_mechanism_ssl,rabbitmq_aws,
rabbitmq_consistent_hash_exchange,rabbitmq_event_exchange, rabbitmq_consistent_hash_exchange,rabbitmq_event_exchange,

View File

@ -44,6 +44,13 @@
"monitoring" "monitoring"
], ],
"limits": {} "limits": {}
},
{
"name": "rabbit_no_management",
"password_hash": "Joz9zzUBOrX10lB3GisWN5oTXK+wj0gxS/nyrfTYmBOuhps5",
"hashing_algorithm": "rabbit_password_hashing_sha256",
"tags": [ ],
"limits": {}
} }
], ],
"vhosts": [ "vhosts": [
@ -65,6 +72,13 @@
"configure": ".*", "configure": ".*",
"write": ".*", "write": ".*",
"read": ".*" "read": ".*"
},
{
"user": "rabbit_no_management",
"vhost": "/",
"configure": ".*",
"write": ".*",
"read": ".*"
} }
] ]

View File

@ -0,0 +1,59 @@
const { By, Key, until, Builder } = require('selenium-webdriver')
require('chromedriver')
const assert = require('assert')
const { buildDriver, goToHome, captureScreensFor, teardown, idpLoginPage } = require('../../utils')
const SSOHomePage = require('../../pageobjects/SSOHomePage')
const OverviewPage = require('../../pageobjects/OverviewPage')
describe('An user without management tag', function () {
let homePage
let idpLogin
let overview
let captureScreen
before(async function () {
driver = buildDriver()
await goToHome(driver)
homePage = new SSOHomePage(driver)
idpLogin = idpLoginPage(driver)
overview = new OverviewPage(driver)
captureScreen = captureScreensFor(driver, __filename)
await homePage.clickToLogin()
await idpLogin.login('rabbit_no_management', 'rabbit_no_management')
if (!await homePage.isLoaded()) {
throw new Error('Failed to login')
}
})
it('cannot log in into the management ui', async function () {
const visible = await homePage.isWarningVisible()
assert.ok(visible)
})
it('should get "Not authorized" warning message', async function(){
assert.equal('Not authorized', await homePage.getWarning())
assert.equal('Click here to logout', await homePage.getLogoutButton())
assert.ok(!await homePage.isBasicAuthSectionVisible())
assert.ok(!await homePage.isOAuth2SectionVisible())
})
describe("After clicking on logout button", function() {
before(async function () {
await homePage.clickToLogout()
})
it('should get redirected to home page again without error message', async function(){
const visible = await homePage.isWarningVisible()
assert.ok(!visible)
})
})
after(async function () {
await teardown(driver, this, captureScreen)
})
})

View File

@ -6,7 +6,7 @@ const { buildDriver, goToHome, captureScreensFor, teardown, idpLoginPage } = req
const SSOHomePage = require('../../pageobjects/SSOHomePage') const SSOHomePage = require('../../pageobjects/SSOHomePage')
const OverviewPage = require('../../pageobjects/OverviewPage') const OverviewPage = require('../../pageobjects/OverviewPage')
describe('An user without administrator tag', function () { describe('An user without management tag', function () {
let homePage let homePage
let idpLogin let idpLogin
let overview let overview

View File

@ -13,6 +13,9 @@ const EXCHANGES_TAB = By.css('div#menu ul#tabs li#exchanges')
const ADMIN_TAB = By.css('div#menu ul#tabs li#admin') const ADMIN_TAB = By.css('div#menu ul#tabs li#admin')
const STREAM_CONNECTIONS_TAB = By.css('div#menu ul#tabs li#stream-connections') const STREAM_CONNECTIONS_TAB = By.css('div#menu ul#tabs li#stream-connections')
const FORM_POPUP = By.css('div.form-popup-warn')
const FORM_POPUP_CLOSE_BUTTON = By.css('div.form-popup-warn span')
module.exports = class BasePage { module.exports = class BasePage {
driver driver
timeout timeout
@ -24,7 +27,6 @@ module.exports = class BasePage {
this.timeout = parseInt(process.env.SELENIUM_TIMEOUT) || 1000 // max time waiting to locate an element. Should be less that test timeout this.timeout = parseInt(process.env.SELENIUM_TIMEOUT) || 1000 // max time waiting to locate an element. Should be less that test timeout
this.polling = parseInt(process.env.SELENIUM_POLLING) || 500 // how frequent selenium searches for an element this.polling = parseInt(process.env.SELENIUM_POLLING) || 500 // how frequent selenium searches for an element
this.interactionDelay = parseInt(process.env.SELENIUM_INTERACTION_DELAY) || 0 // slow down interactions (when rabbit is behind a http proxy) this.interactionDelay = parseInt(process.env.SELENIUM_INTERACTION_DELAY) || 0 // slow down interactions (when rabbit is behind a http proxy)
console.log("Interaction Delay : " + this.interactionDelay)
} }
@ -138,7 +140,14 @@ module.exports = class BasePage {
return table_model return table_model
} }
async isPopupWarningDisplayed() { async isPopupWarningDisplayed() {
const element = "form-popup-warn" try {
let element = await driver.findElement(FORM_POPUP)
return element.isDisplayed()
} catch(e) {
return Promise.resolve(false)
}
/*
let element = await driver.findElement(FORM_POPUP)
return this.driver.wait(until.elementIsVisible(element), this.timeout / 2, return this.driver.wait(until.elementIsVisible(element), this.timeout / 2,
'Timed out after [timeout=' + this.timeout + ';polling=' + this.polling + '] awaiting till visible ' + element, 'Timed out after [timeout=' + this.timeout + ';polling=' + this.polling + '] awaiting till visible ' + element,
this.polling / 2).then(function onWarningVisible(e) { this.polling / 2).then(function onWarningVisible(e) {
@ -146,17 +155,20 @@ module.exports = class BasePage {
}, function onError(e) { }, function onError(e) {
return Promise.resolve(false) return Promise.resolve(false)
}) })
*/
} }
async getPopupWarning() { async getPopupWarning() {
const element = "form-popup-warn" let element = await driver.findElement(FORM_POPUP)
return this.driver.wait(until.elementIsVisible(element), this.timeout, return this.driver.wait(until.elementIsVisible(element), this.timeout,
'Timed out after [timeout=' + this.timeout + ';polling=' + this.polling + '] awaiting till visible ' + element, 'Timed out after [timeout=' + this.timeout + ';polling=' + this.polling + '] awaiting till visible ' + element,
this.polling) this.polling).getText().then((value) => value.substring(0, value.search('\n\nClose')))
}
async closePopupWarning() {
return this.click(FORM_POPUP_CLOSE_BUTTON)
} }
async isDisplayed(locator) { async isDisplayed(locator) {
try { try {
element = await driver.findElement(locator) let element = await driver.findElement(locator)
return this.driver.wait(until.elementIsVisible(element), this.timeout, return this.driver.wait(until.elementIsVisible(element), this.timeout,
'Timed out after [timeout=' + this.timeout + ';polling=' + this.polling + '] awaiting till visible ' + element, 'Timed out after [timeout=' + this.timeout + ';polling=' + this.polling + '] awaiting till visible ' + element,

View File

@ -6,6 +6,7 @@ const FORM = By.css('div#login form')
const USERNAME = By.css('input[name="username"]') const USERNAME = By.css('input[name="username"]')
const PASSWORD = By.css('input[name="password"]') const PASSWORD = By.css('input[name="password"]')
const LOGIN_BUTTON = By.css('div#outer div#login form input[type=submit]') const LOGIN_BUTTON = By.css('div#outer div#login form input[type=submit]')
const WARNING = By.css('div#outer div#login div#login-status p')
module.exports = class LoginPage extends BasePage { module.exports = class LoginPage extends BasePage {
async isLoaded () { async isLoaded () {
@ -22,4 +23,26 @@ module.exports = class LoginPage extends BasePage {
async getLoginButton () { async getLoginButton () {
return this.getValue(LOGIN_BUTTON) return this.getValue(LOGIN_BUTTON)
} }
async isWarningVisible () {
try {
await this.waitForDisplayed(WARNING)
return Promise.resolve(true)
} catch (e) {
return Promise.resolve(false)
}
}
async getWarnings() {
try
{
return driver.findElements(WARNING)
} catch (NoSuchElement) {
return Promise.resolve([])
}
}
async getWarning () {
return this.getText(WARNING)
}
} }

View File

@ -9,28 +9,49 @@
-export([init/2]). -export([init/2]).
-include_lib("amqp_client/include/amqp_client.hrl").
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
init(Req0, State) -> init(Req0, State) ->
bootstrap_oauth(rabbit_mgmt_headers:set_no_cache_headers( bootstrap_oauth(rabbit_mgmt_headers:set_no_cache_headers(
rabbit_mgmt_headers:set_common_permission_headers(Req0, ?MODULE), ?MODULE), State). rabbit_mgmt_headers:set_common_permission_headers(Req0, ?MODULE), ?MODULE), State).
bootstrap_oauth(Req0, State) -> bootstrap_oauth(Req0, State) ->
JSContent = oauth_initialize_if_required() ++ set_token_auth(Req0), AuthSettings = rabbit_mgmt_wm_auth:authSettings(),
{ok, cowboy_req:reply(200, #{<<"content-type">> => <<"text/javascript; charset=utf-8">>}, JSContent, Req0), State}. Dependencies = oauth_dependencies(),
JSContent = import_dependencies(Dependencies) ++
set_oauth_settings(AuthSettings) ++
set_token_auth(AuthSettings, Req0) ++
export_dependencies(Dependencies),
{ok, cowboy_req:reply(200, #{<<"content-type">> => <<"text/javascript; charset=utf-8">>},
JSContent, Req0), State}.
oauth_initialize_if_required() -> set_oauth_settings(AuthSettings) ->
["function oauth_initialize_if_required() { return oauth_initialize(" , JsonAuthSettings = rabbit_json:encode(rabbit_mgmt_format:format_nulls(AuthSettings)),
rabbit_json:encode(rabbit_mgmt_format:format_nulls(rabbit_mgmt_wm_auth:authSettings())) , ") }" ]. ["set_oauth_settings(", JsonAuthSettings, ");"].
set_token_auth(Req0) -> set_token_auth(AuthSettings, Req0) ->
case application:get_env(rabbitmq_management, oauth_enabled, false) of case proplists:get_value(oauth_enabled, AuthSettings, false) of
true -> true ->
case cowboy_req:parse_header(<<"authorization">>, Req0) of case cowboy_req:parse_header(<<"authorization">>, Req0) of
{bearer, Token} -> ["set_token_auth('", Token, "');"]; {bearer, Token} -> ["set_token_auth('", Token, "');"];
_ -> [] _ -> []
end; end;
false -> [] false ->
end. []
end.
import_dependencies(Dependencies) ->
["import {", string:join(Dependencies, ","), "} from './helper.js';"].
oauth_dependencies() ->
["oauth_initialize_if_required",
"hasAnyResourceServerReady",
"oauth_initialize", "oauth_initiate",
"oauth_initiateLogin",
"oauth_initiateLogout",
"oauth_completeLogin",
"oauth_completeLogout",
"set_oauth_settings"].
export_dependencies(Dependencies) ->
[ io_lib:format("window.~s = ~s;", [Dep, Dep]) || Dep <- Dependencies ].

View File

@ -444,7 +444,7 @@ auth_test(Config) ->
%% because user/password are ok, tags are not %% because user/password are ok, tags are not
test_auth(Config, ?NOT_AUTHORISED, [auth_header("user", "user")]), test_auth(Config, ?NOT_AUTHORISED, [auth_header("user", "user")]),
WrongAuthResponseHeaders = test_auth(Config, ?NOT_AUTHORISED, [auth_header("guest", "gust")]), WrongAuthResponseHeaders = test_auth(Config, ?NOT_AUTHORISED, [auth_header("guest", "gust")]),
?assertEqual(true, lists:keymember("www-authenticate", 1, WrongAuthResponseHeaders)), %?assertEqual(true, lists:keymember("www-authenticate", 1, WrongAuthResponseHeaders)),
test_auth(Config, ?OK, [auth_header("guest", "guest")]), test_auth(Config, ?OK, [auth_header("guest", "guest")]),
http_delete(Config, "/users/user", {group, '2xx'}), http_delete(Config, "/users/user", {group, '2xx'}),
passed. passed.

View File

@ -265,7 +265,9 @@ internal_user(User) ->
user(User) -> user(User) ->
[{name, User#user.username}, [{name, User#user.username},
{tags, tags_as_binaries(User#user.tags)}]. {tags, tags_as_binaries(User#user.tags)},
{is_internal_user, lists:any(fun({Module,_}) -> Module == rabbit_auth_backend_internal end,
User#user.authz_backends)}].
tags_as_binaries(Tags) -> tags_as_binaries(Tags) ->
[to_binary(T) || T <- Tags]. [to_binary(T) || T <- Tags].

View File

@ -277,15 +277,8 @@ halt_response(Code, Type, Reason, ReqData, Context) ->
rabbit_json:encode(Json), ReqData), rabbit_json:encode(Json), ReqData),
{stop, ReqData1, Context}. {stop, ReqData1, Context}.
not_authenticated(Reason, ReqData, Context, not_authenticated(Reason, ReqData, Context, _AuthConfig) ->
#auth_settings{auth_realm = AuthRealm} = AuthConfig) -> halt_response(401, not_authorized, Reason, ReqData, Context).
case is_oauth2_enabled(AuthConfig) of
false ->
ReqData1 = cowboy_req:set_resp_header(<<"www-authenticate">>, AuthRealm, ReqData),
halt_response(401, not_authorized, Reason, ReqData1, Context);
true ->
halt_response(401, not_authorized, Reason, ReqData, Context)
end.
format_reason(Tuple) when is_tuple(Tuple) -> format_reason(Tuple) when is_tuple(Tuple) ->
tuple(Tuple); tuple(Tuple);
@ -366,6 +359,3 @@ log_access_control_result(NotOK) ->
is_basic_auth_disabled(#auth_settings{basic_auth_enabled = Enabled}) -> is_basic_auth_disabled(#auth_settings{basic_auth_enabled = Enabled}) ->
not Enabled. not Enabled.
is_oauth2_enabled(#auth_settings{oauth2_enabled = Enabled}) ->
Enabled.