diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_login.erl b/deps/rabbitmq_management/src/rabbit_mgmt_login.erl index 4423565c12e5..e91839dd7bc0 100644 --- a/deps/rabbitmq_management/src/rabbit_mgmt_login.erl +++ b/deps/rabbitmq_management/src/rabbit_mgmt_login.erl @@ -52,7 +52,7 @@ login(<<"POST">>, Req0, State) -> AccessToken -> handleAccessToken(Req0, AccessToken, State) end; -login(<<"GET">>, Req, State) -> +login(<<"GET">>, Req, State) -> Auth = case rabbit_mgmt_util:qs_val(?MANAGEMENT_LOGIN_STRICT_AUTH_MECHANISM, Req) of undefined -> case rabbit_mgmt_util:qs_val(?MANAGEMENT_LOGIN_PREFERRED_AUTH_MECHANISM, Req) of diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_oauth_bootstrap.erl b/deps/rabbitmq_management/src/rabbit_mgmt_oauth_bootstrap.erl index d9c51a93b33d..7e0ef38675f9 100644 --- a/deps/rabbitmq_management/src/rabbit_mgmt_oauth_bootstrap.erl +++ b/deps/rabbitmq_management/src/rabbit_mgmt_oauth_bootstrap.erl @@ -12,24 +12,24 @@ -include_lib("kernel/include/logger.hrl"). %%-------------------------------------------------------------------- -%% js/oidc-oauth/bootstrap.js -%% It produces a javascript file with all the oauth2 configuration needed +%% js/oidc-oauth/bootstrap.js +%% It produces a javascript file with all the oauth2 configuration needed %% in the client-side of the management ui. %% This endpoint only accepts GET method. %% -%% It can work in conjunction with the /api/login endpoint. If the users are +%% It can work in conjunction with the /api/login endpoint. If the users are %% redirected to the home page of the management ui, and eventually to this endpoint, -%% via the /api/login endpoint is very likely that the request carries a cookie. +%% via the /api/login endpoint is very likely that the request carries a cookie. %% It can be the <<"access_token">> cookie or the cookies <<"strict_auth_mechanism">> %% or <<"preferred_auth_mechanism">>. %% These cookies are consumed by this endpoint and removed afterwards. %% %% Additionally, this endpoint may accept users' authentication mechanism preferences -%% via its corresponding header, in addition to the two cookies mentioned above. -%% But not via request parameters. If this endpoint would have accepted request parameters, -%% it would have to use the "Referer" header to extract the original request parameters. +%% via its corresponding header, in addition to the two cookies mentioned above. +%% But not via request parameters. If this endpoint would have accepted request parameters, +%% it would have to use the "Referer" header to extract the original request parameters. %% It is possible that in some environments, these headers may be dropped before they reach this endpoint. -%% Therefore, users who can only use request parameters, they have to use the /api/login +%% Therefore, users who can only use request parameters, they have to use the /api/login %% endpoint instead. init(Req0, State) -> @@ -45,7 +45,7 @@ bootstrap_oauth(Req0, State) -> set_oauth_settings(AuthSettings) ++ SetTokenAuth ++ export_dependencies(Dependencies), - + {ok, cowboy_req:reply(200, #{<<"content-type">> => <<"text/javascript; charset=utf-8">>}, JSContent, Req2), State}. @@ -56,11 +56,11 @@ enrich_oauth_settings(Req0, AuthSettings) -> {preferred_auth_mechanism, Args} -> {Req1, [{preferred_auth_mechanism, Args} | AuthSettings]}; {strict_auth_mechanism, Args} -> {Req1, [{strict_auth_mechanism, Args} | AuthSettings]}; {error, Reason} -> ?LOG_DEBUG("~p", [Reason]), - {Req1, AuthSettings} + {Req1, AuthSettings} end. get_auth_mechanism(Req) -> - case get_auth_mechanism_from_cookies(Req) of - undefined -> + case get_auth_mechanism_from_cookies(Req) of + undefined -> case cowboy_req:header(<<"x-", ?MANAGEMENT_LOGIN_STRICT_AUTH_MECHANISM/binary>>, Req) of undefined -> case cowboy_req:header(<<"x-", ?MANAGEMENT_LOGIN_PREFERRED_AUTH_MECHANISM/binary>>, Req) of @@ -69,37 +69,37 @@ get_auth_mechanism(Req) -> end; Val -> {Req, {strict_auth_mechanism, Val}} end; - {Type, _} = Auth -> { cowboy_req:set_resp_cookie(term_to_binary(Type), + {Type, _} = Auth -> { cowboy_req:set_resp_cookie(term_to_binary(Type), <<"">>, Req, #{ max_age => 0, http_only => true, path => ?OAUTH2_BOOTSTRAP_PATH, same_site => strict - }), + }), Auth } end. get_auth_mechanism_from_cookies(Req) -> Cookies = cowboy_req:parse_cookies(Req), - case proplists:get_value(?MANAGEMENT_LOGIN_STRICT_AUTH_MECHANISM, Cookies) of - undefined -> - case proplists:get_value(?MANAGEMENT_LOGIN_PREFERRED_AUTH_MECHANISM, Cookies) of + case proplists:get_value(?MANAGEMENT_LOGIN_STRICT_AUTH_MECHANISM, Cookies) of + undefined -> + case proplists:get_value(?MANAGEMENT_LOGIN_PREFERRED_AUTH_MECHANISM, Cookies) of undefined -> undefined; Val -> {preferred_auth_mechanism, Val} end; Val -> {strict_auth_mechanism, Val} end. -validate_auth_mechanism({Type, <<"oauth2:", Id/binary>>}, AuthSettings) -> - case maps:is_key(Id, proplists:get_value(oauth_resource_servers, AuthSettings)) of +validate_auth_mechanism({Type, <<"oauth2:", Id/binary>>}, AuthSettings) -> + case maps:is_key(Id, proplists:get_value(oauth_resource_servers, AuthSettings)) of true -> {Type, [{type, <<"oauth2">>}, {resource_id, Id}]}; _ -> {error, {unknown_resource_id, Id}} end; -validate_auth_mechanism({Type, <<"basic">>}, _AuthSettings) -> +validate_auth_mechanism({Type, <<"basic">>}, _AuthSettings) -> {Type, [{type, <<"basic">>}]}; validate_auth_mechanism({_, _}, _AuthSettings) -> {error, unknown_auth_mechanism}; validate_auth_mechanism(_, _) -> {error, unknown_auth_mechanism}. - + set_oauth_settings(AuthSettings) -> JsonAuthSettings = rabbit_json:encode(rabbit_mgmt_format:format_nulls(AuthSettings)), ["set_oauth_settings(", JsonAuthSettings, ");"]. @@ -108,15 +108,19 @@ set_token_auth(AuthSettings, Req0) -> case proplists:get_value(oauth_enabled, AuthSettings, false) of true -> case cowboy_req:parse_header(<<"authorization">>, Req0) of - {bearer, Token} -> + {bearer, Token} -> { - Req0, + Req0, ["set_token_auth('", Token, "');"] }; - _ -> - Cookies = cowboy_req:parse_cookies(Req0), - case lists:keyfind(?OAUTH2_ACCESS_TOKEN, 1, Cookies) of - {_, Token} -> + _ -> + Cookies = cowboy_req:parse_cookies(Req0), + case proplists:get_value(?OAUTH2_ACCESS_TOKEN, Cookies) of + undefined -> { + Req0, + [] + }; + Token -> { cowboy_req:set_resp_cookie( ?OAUTH2_ACCESS_TOKEN, <<"">>, Req0, #{ @@ -124,17 +128,13 @@ set_token_auth(AuthSettings, Req0) -> http_only => true, path => ?OAUTH2_BOOTSTRAP_PATH, same_site => strict - }), + }), ["set_token_auth('", Token, "');"] - }; - false -> { - Req0, - [] } end end; false -> { - Req0, + Req0, [] } end. diff --git a/selenium/short-suite-management-ui b/selenium/short-suite-management-ui index 03828544c99e..2d8209f3e321 100644 --- a/selenium/short-suite-management-ui +++ b/selenium/short-suite-management-ui @@ -9,4 +9,5 @@ mgt/queuesAndStreams.sh mgt/limits.sh mgt/amqp10-connections.sh mgt/mqtt-connections.sh -mgt/feature-flags.sh \ No newline at end of file +mgt/feature-flags.sh +authnz-mgt/multi-oauth-with-basic-auth.sh \ No newline at end of file diff --git a/selenium/test/multi-oauth/with-basic-auth/landing-with-login-preferences.js b/selenium/test/multi-oauth/with-basic-auth/landing-with-login-preferences.js new file mode 100644 index 000000000000..06d4caa8d5a0 --- /dev/null +++ b/selenium/test/multi-oauth/with-basic-auth/landing-with-login-preferences.js @@ -0,0 +1,84 @@ +const { By, Key, until, Builder } = require('selenium-webdriver') +require('chromedriver') +const assert = require('assert') +const { buildDriver, goToHome, goToLogin, captureScreensFor, teardown, findOption } = require('../../utils') + +const SSOHomePage = require('../../pageobjects/SSOHomePage') + +describe('Given two oauth resources and basic auth enabled, an unauthenticated user', function () { + let driver; + let captureScreen; + let login; + + before(async function () { + this.driver = buildDriver(); + this.captureScreen = captureScreensFor(this.driver, __filename); + + login = async (key, value) => { + await goToLogin(this.driver, key, value); + const homePage = new SSOHomePage(this.driver); + await homePage.isLoaded(); + return homePage; + } + }) + + it('can preselect rabbit_dev oauth2 resource', async function () { + const homePage = await login("preferred_auth_mechanism", "oauth2:rabbit_dev"); + + const oauth2Section = await homePage.isOAuth2SectionVisible(); + assert.ok((await oauth2Section.getAttribute("class")).includes("section-visible")) + const basicSection = await homePage.isBasicAuthSectionVisible(); + assert.ok((await basicSection.getAttribute("class")).includes("section-invisible")) + + resources = await homePage.getOAuthResourceOptions(); + const option = findOption("rabbit_dev", resources); + assert.ok(option); + assert.ok(option.selected); + + }) + it('can preselect rabbit_prod oauth2 resource', async function () { + const homePage = await login("preferred_auth_mechanism", "oauth2:rabbit_prod"); + + const oauth2Section = await homePage.isOAuth2SectionVisible(); + assert.ok((await oauth2Section.getAttribute("class")).includes("section-visible")) + const basicSection = await homePage.isBasicAuthSectionVisible(); + assert.ok((await basicSection.getAttribute("class")).includes("section-invisible")) + + resources = await homePage.getOAuthResourceOptions(); + const option = findOption("rabbit_prod", resources); + assert.ok(option); + assert.ok(option.selected); + + }) + + it('can preselect basic auth', async function () { + const homePage = await login("preferred_auth_mechanism", "basic"); + + const oauth2Section = await homePage.isOAuth2SectionVisible(); + assert.ok((await oauth2Section.getAttribute("class")).includes("section-invisible")) + const basicSection = await homePage.isBasicAuthSectionVisible(); + assert.ok((await basicSection.getAttribute("class")).includes("section-visible")) + }) + + it('can force only to authenticate only with rabbit_dev oauth2 resource', async function () { + const homePage = await login("strict_auth_mechanism", "oauth2:rabbit_dev"); + const value = await homePage.getLoginButtonOnClick(); + assert.ok(value.includes("rabbit_dev")); + }) + it('can force only to authenticate only with rabbit_prod oauth2 resource', async function () { + const homePage = await login("strict_auth_mechanism", "oauth2:rabbit_prod"); + const value = await homePage.getLoginButtonOnClick(); + assert.ok(value.includes("rabbit_prod")); + }) + it('can force only to authenticate only with basic auth', async function () { + const homePage = await login("strict_auth_mechanism", "basic"); + await homePage.isOAuth2SectionNotVisible(); + const basicSection = await homePage.isBasicAuthSectionVisible(); + assert.ok((await basicSection.getAttribute("class")).includes("section-visible")) + + }) + + after(async function () { + await teardown(this.driver, this, this.captureScreen); + }) +}) diff --git a/selenium/test/pageobjects/BasePage.js b/selenium/test/pageobjects/BasePage.js index 63106a1fcbe8..3b4a318aee4b 100644 --- a/selenium/test/pageobjects/BasePage.js +++ b/selenium/test/pageobjects/BasePage.js @@ -131,7 +131,8 @@ module.exports = class BasePage { for (const index in optionList) { const t = await optionList[index].getText() const v = await optionList[index].getAttribute('value') - table_model.push({"text":t, "value": v}) + const s = await optionList[index].getAttribute('selected') + table_model.push({"text": t, "value": v, "selected" : s !== undefined}) } return table_model @@ -300,13 +301,14 @@ module.exports = class BasePage { async isDisplayed(locator) { try { - let element = await driver.findElement(locator) + let element = await this.driver.findElement(locator) return this.driver.wait(until.elementIsVisible(element), this.timeout, 'Timed out after [timeout=' + this.timeout + ';polling=' + this.polling + '] awaiting till visible ' + element, this.polling / 2) }catch(error) { - return Promise.resolve(false) + console.log("isDisplayed failed due to " + error); + return Promise.resolve(false); } } diff --git a/selenium/test/pageobjects/SSOHomePage.js b/selenium/test/pageobjects/SSOHomePage.js index 44f771bc54e2..995b7e7246eb 100644 --- a/selenium/test/pageobjects/SSOHomePage.js +++ b/selenium/test/pageobjects/SSOHomePage.js @@ -37,6 +37,10 @@ module.exports = class SSOHomePage extends BasePage { async getLoginButton () { return this.getText(OAUTH2_LOGIN_BUTTON) } + async getLoginButtonOnClick () { + const element = await this.waitForDisplayed(OAUTH2_LOGIN_BUTTON); + return element.getAttribute('onClick'); + } async getLogoutButton () { return this.getText(LOGOUT_BUTTON) } @@ -74,6 +78,10 @@ module.exports = class SSOHomePage extends BasePage { async isOAuth2SectionVisible() { return this.isDisplayed(SECTION_LOGIN_WITH_OAUTH) } + async isOAuth2SectionNotVisible() { + return this.isElementNotVisible(SECTION_LOGIN_WITH_OAUTH) + } + async getOAuth2Section() { return this.waitForDisplayed(SECTION_LOGIN_WITH_OAUTH) } diff --git a/selenium/test/utils.js b/selenium/test/utils.js index c862b290cc04..db1c94a88a8b 100644 --- a/selenium/test/utils.js +++ b/selenium/test/utils.js @@ -39,8 +39,9 @@ class CaptureScreenshot { const screenshotsSubDir = path.join(screenshotsDir, this.test) if (!fs.existsSync(screenshotsSubDir)) { await fsp.mkdir(screenshotsSubDir) - } + } const dest = path.join(screenshotsSubDir, name + '.png') + console.log("screenshot saved to " + dest) await fsp.writeFile(dest, image, 'base64') } } @@ -122,8 +123,31 @@ module.exports = { return d.driver.get(d.baseUrl) }, - goToLogin: (d, token) => { - return d.driver.get(d.baseUrl + '#/login?access_token=' + token) + /** + * For instance, + * goToLogin(d, access_token, myAccessToken) + * or + * goToLogin(d, preferred_auth_mechanism, "oauth2:my-resource") + */ + goToLogin: (d, ...keyValuePairs) => { + const params = []; + for (let i = 0; i < keyValuePairs.length; i += 2) { + const key = keyValuePairs[i]; + const value = keyValuePairs[i + 1]; + + if (key !== undefined) { + // URL-encode both key and value + const encodedKey = encodeURIComponent(key); + const encodedValue = encodeURIComponent(value || ''); + params.push(`${encodedKey}=${encodedValue}`); + } + } + // Build query string: "key1=value1&key2=value2" + const queryString = params.join('&'); + + const url = d.baseUrl + '/login?' + queryString; + console.log("Navigating to " + url); + return d.driver.get(url); }, goToConnections: (d) => { @@ -263,8 +287,15 @@ module.exports = { && actualOption.text == expectedOptions[i].text)) } }, + findOption: (value, options) => { + for (let i = 0; i < options.length; i++) { + if (options[i].value === value) return options[i]; + } + return undefined; + }, teardown: async (d, test, captureScreen = null) => { + driver = d.driver driver.manage().logs().get(logging.Type.BROWSER).then(function(entries) { entries.forEach(function(entry) { @@ -274,8 +305,11 @@ module.exports = { if (test.currentTest) { if (test.currentTest.isPassed()) { driver.executeScript('lambda-status=passed') - } else { - if (captureScreen != null) await captureScreen.shot('after-failed') + } else { + if (captureScreen != null) { + console.log("Teardown failed . capture..."); + await captureScreen.shot('after-failed'); + } driver.executeScript('lambda-status=failed') } }