Skip to content

Commit 140e828

Browse files
Merge pull request #15137 from rabbitmq/mergify/bp/v4.2.x/pr-15101
Improve selenium coverage of Rmq 2359 (backport #15101)
2 parents 39fbf91 + e7e8040 commit 140e828

File tree

7 files changed

+172
-43
lines changed

7 files changed

+172
-43
lines changed

deps/rabbitmq_management/src/rabbit_mgmt_login.erl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ login(<<"POST">>, Req0, State) ->
5252
AccessToken -> handleAccessToken(Req0, AccessToken, State)
5353
end;
5454

55-
login(<<"GET">>, Req, State) ->
55+
login(<<"GET">>, Req, State) ->
5656
Auth = case rabbit_mgmt_util:qs_val(?MANAGEMENT_LOGIN_STRICT_AUTH_MECHANISM, Req) of
5757
undefined ->
5858
case rabbit_mgmt_util:qs_val(?MANAGEMENT_LOGIN_PREFERRED_AUTH_MECHANISM, Req) of

deps/rabbitmq_management/src/rabbit_mgmt_oauth_bootstrap.erl

Lines changed: 33 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -12,24 +12,24 @@
1212
-include_lib("kernel/include/logger.hrl").
1313

1414
%%--------------------------------------------------------------------
15-
%% js/oidc-oauth/bootstrap.js
16-
%% It produces a javascript file with all the oauth2 configuration needed
15+
%% js/oidc-oauth/bootstrap.js
16+
%% It produces a javascript file with all the oauth2 configuration needed
1717
%% in the client-side of the management ui.
1818
%% This endpoint only accepts GET method.
1919
%%
20-
%% It can work in conjunction with the /api/login endpoint. If the users are
20+
%% It can work in conjunction with the /api/login endpoint. If the users are
2121
%% redirected to the home page of the management ui, and eventually to this endpoint,
22-
%% via the /api/login endpoint is very likely that the request carries a cookie.
22+
%% via the /api/login endpoint is very likely that the request carries a cookie.
2323
%% It can be the <<"access_token">> cookie or the cookies <<"strict_auth_mechanism">>
2424
%% or <<"preferred_auth_mechanism">>.
2525
%% These cookies are consumed by this endpoint and removed afterwards.
2626
%%
2727
%% Additionally, this endpoint may accept users' authentication mechanism preferences
28-
%% via its corresponding header, in addition to the two cookies mentioned above.
29-
%% But not via request parameters. If this endpoint would have accepted request parameters,
30-
%% it would have to use the "Referer" header to extract the original request parameters.
28+
%% via its corresponding header, in addition to the two cookies mentioned above.
29+
%% But not via request parameters. If this endpoint would have accepted request parameters,
30+
%% it would have to use the "Referer" header to extract the original request parameters.
3131
%% It is possible that in some environments, these headers may be dropped before they reach this endpoint.
32-
%% Therefore, users who can only use request parameters, they have to use the /api/login
32+
%% Therefore, users who can only use request parameters, they have to use the /api/login
3333
%% endpoint instead.
3434

3535
init(Req0, State) ->
@@ -45,7 +45,7 @@ bootstrap_oauth(Req0, State) ->
4545
set_oauth_settings(AuthSettings) ++
4646
SetTokenAuth ++
4747
export_dependencies(Dependencies),
48-
48+
4949
{ok, cowboy_req:reply(200, #{<<"content-type">> => <<"text/javascript; charset=utf-8">>},
5050
JSContent, Req2), State}.
5151

@@ -56,11 +56,11 @@ enrich_oauth_settings(Req0, AuthSettings) ->
5656
{preferred_auth_mechanism, Args} -> {Req1, [{preferred_auth_mechanism, Args} | AuthSettings]};
5757
{strict_auth_mechanism, Args} -> {Req1, [{strict_auth_mechanism, Args} | AuthSettings]};
5858
{error, Reason} -> ?LOG_DEBUG("~p", [Reason]),
59-
{Req1, AuthSettings}
59+
{Req1, AuthSettings}
6060
end.
6161
get_auth_mechanism(Req) ->
62-
case get_auth_mechanism_from_cookies(Req) of
63-
undefined ->
62+
case get_auth_mechanism_from_cookies(Req) of
63+
undefined ->
6464
case cowboy_req:header(<<"x-", ?MANAGEMENT_LOGIN_STRICT_AUTH_MECHANISM/binary>>, Req) of
6565
undefined ->
6666
case cowboy_req:header(<<"x-", ?MANAGEMENT_LOGIN_PREFERRED_AUTH_MECHANISM/binary>>, Req) of
@@ -69,37 +69,37 @@ get_auth_mechanism(Req) ->
6969
end;
7070
Val -> {Req, {strict_auth_mechanism, Val}}
7171
end;
72-
{Type, _} = Auth -> { cowboy_req:set_resp_cookie(term_to_binary(Type),
72+
{Type, _} = Auth -> { cowboy_req:set_resp_cookie(term_to_binary(Type),
7373
<<"">>, Req, #{
7474
max_age => 0,
7575
http_only => true,
7676
path => ?OAUTH2_BOOTSTRAP_PATH,
7777
same_site => strict
78-
}),
78+
}),
7979
Auth
8080
}
8181
end.
8282

8383
get_auth_mechanism_from_cookies(Req) ->
8484
Cookies = cowboy_req:parse_cookies(Req),
85-
case proplists:get_value(?MANAGEMENT_LOGIN_STRICT_AUTH_MECHANISM, Cookies) of
86-
undefined ->
87-
case proplists:get_value(?MANAGEMENT_LOGIN_PREFERRED_AUTH_MECHANISM, Cookies) of
85+
case proplists:get_value(?MANAGEMENT_LOGIN_STRICT_AUTH_MECHANISM, Cookies) of
86+
undefined ->
87+
case proplists:get_value(?MANAGEMENT_LOGIN_PREFERRED_AUTH_MECHANISM, Cookies) of
8888
undefined -> undefined;
8989
Val -> {preferred_auth_mechanism, Val}
9090
end;
9191
Val -> {strict_auth_mechanism, Val}
9292
end.
93-
validate_auth_mechanism({Type, <<"oauth2:", Id/binary>>}, AuthSettings) ->
94-
case maps:is_key(Id, proplists:get_value(oauth_resource_servers, AuthSettings)) of
93+
validate_auth_mechanism({Type, <<"oauth2:", Id/binary>>}, AuthSettings) ->
94+
case maps:is_key(Id, proplists:get_value(oauth_resource_servers, AuthSettings)) of
9595
true -> {Type, [{type, <<"oauth2">>}, {resource_id, Id}]};
9696
_ -> {error, {unknown_resource_id, Id}}
9797
end;
98-
validate_auth_mechanism({Type, <<"basic">>}, _AuthSettings) ->
98+
validate_auth_mechanism({Type, <<"basic">>}, _AuthSettings) ->
9999
{Type, [{type, <<"basic">>}]};
100100
validate_auth_mechanism({_, _}, _AuthSettings) -> {error, unknown_auth_mechanism};
101101
validate_auth_mechanism(_, _) -> {error, unknown_auth_mechanism}.
102-
102+
103103
set_oauth_settings(AuthSettings) ->
104104
JsonAuthSettings = rabbit_json:encode(rabbit_mgmt_format:format_nulls(AuthSettings)),
105105
["set_oauth_settings(", JsonAuthSettings, ");"].
@@ -108,33 +108,33 @@ set_token_auth(AuthSettings, Req0) ->
108108
case proplists:get_value(oauth_enabled, AuthSettings, false) of
109109
true ->
110110
case cowboy_req:parse_header(<<"authorization">>, Req0) of
111-
{bearer, Token} ->
111+
{bearer, Token} ->
112112
{
113-
Req0,
113+
Req0,
114114
["set_token_auth('", Token, "');"]
115115
};
116-
_ ->
117-
Cookies = cowboy_req:parse_cookies(Req0),
118-
case lists:keyfind(?OAUTH2_ACCESS_TOKEN, 1, Cookies) of
119-
{_, Token} ->
116+
_ ->
117+
Cookies = cowboy_req:parse_cookies(Req0),
118+
case proplists:get_value(?OAUTH2_ACCESS_TOKEN, Cookies) of
119+
undefined -> {
120+
Req0,
121+
[]
122+
};
123+
Token ->
120124
{
121125
cowboy_req:set_resp_cookie(
122126
?OAUTH2_ACCESS_TOKEN, <<"">>, Req0, #{
123127
max_age => 0,
124128
http_only => true,
125129
path => ?OAUTH2_BOOTSTRAP_PATH,
126130
same_site => strict
127-
}),
131+
}),
128132
["set_token_auth('", Token, "');"]
129-
};
130-
false -> {
131-
Req0,
132-
[]
133133
}
134134
end
135135
end;
136136
false -> {
137-
Req0,
137+
Req0,
138138
[]
139139
}
140140
end.

selenium/short-suite-management-ui

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,5 @@ mgt/queuesAndStreams.sh
99
mgt/limits.sh
1010
mgt/amqp10-connections.sh
1111
mgt/mqtt-connections.sh
12-
mgt/feature-flags.sh
12+
mgt/feature-flags.sh
13+
authnz-mgt/multi-oauth-with-basic-auth.sh
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
const { By, Key, until, Builder } = require('selenium-webdriver')
2+
require('chromedriver')
3+
const assert = require('assert')
4+
const { buildDriver, goToHome, goToLogin, captureScreensFor, teardown, findOption } = require('../../utils')
5+
6+
const SSOHomePage = require('../../pageobjects/SSOHomePage')
7+
8+
describe('Given two oauth resources and basic auth enabled, an unauthenticated user', function () {
9+
let driver;
10+
let captureScreen;
11+
let login;
12+
13+
before(async function () {
14+
this.driver = buildDriver();
15+
this.captureScreen = captureScreensFor(this.driver, __filename);
16+
17+
login = async (key, value) => {
18+
await goToLogin(this.driver, key, value);
19+
const homePage = new SSOHomePage(this.driver);
20+
await homePage.isLoaded();
21+
return homePage;
22+
}
23+
})
24+
25+
it('can preselect rabbit_dev oauth2 resource', async function () {
26+
const homePage = await login("preferred_auth_mechanism", "oauth2:rabbit_dev");
27+
28+
const oauth2Section = await homePage.isOAuth2SectionVisible();
29+
assert.ok((await oauth2Section.getAttribute("class")).includes("section-visible"))
30+
const basicSection = await homePage.isBasicAuthSectionVisible();
31+
assert.ok((await basicSection.getAttribute("class")).includes("section-invisible"))
32+
33+
resources = await homePage.getOAuthResourceOptions();
34+
const option = findOption("rabbit_dev", resources);
35+
assert.ok(option);
36+
assert.ok(option.selected);
37+
38+
})
39+
it('can preselect rabbit_prod oauth2 resource', async function () {
40+
const homePage = await login("preferred_auth_mechanism", "oauth2:rabbit_prod");
41+
42+
const oauth2Section = await homePage.isOAuth2SectionVisible();
43+
assert.ok((await oauth2Section.getAttribute("class")).includes("section-visible"))
44+
const basicSection = await homePage.isBasicAuthSectionVisible();
45+
assert.ok((await basicSection.getAttribute("class")).includes("section-invisible"))
46+
47+
resources = await homePage.getOAuthResourceOptions();
48+
const option = findOption("rabbit_prod", resources);
49+
assert.ok(option);
50+
assert.ok(option.selected);
51+
52+
})
53+
54+
it('can preselect basic auth', async function () {
55+
const homePage = await login("preferred_auth_mechanism", "basic");
56+
57+
const oauth2Section = await homePage.isOAuth2SectionVisible();
58+
assert.ok((await oauth2Section.getAttribute("class")).includes("section-invisible"))
59+
const basicSection = await homePage.isBasicAuthSectionVisible();
60+
assert.ok((await basicSection.getAttribute("class")).includes("section-visible"))
61+
})
62+
63+
it('can force only to authenticate only with rabbit_dev oauth2 resource', async function () {
64+
const homePage = await login("strict_auth_mechanism", "oauth2:rabbit_dev");
65+
const value = await homePage.getLoginButtonOnClick();
66+
assert.ok(value.includes("rabbit_dev"));
67+
})
68+
it('can force only to authenticate only with rabbit_prod oauth2 resource', async function () {
69+
const homePage = await login("strict_auth_mechanism", "oauth2:rabbit_prod");
70+
const value = await homePage.getLoginButtonOnClick();
71+
assert.ok(value.includes("rabbit_prod"));
72+
})
73+
it('can force only to authenticate only with basic auth', async function () {
74+
const homePage = await login("strict_auth_mechanism", "basic");
75+
await homePage.isOAuth2SectionNotVisible();
76+
const basicSection = await homePage.isBasicAuthSectionVisible();
77+
assert.ok((await basicSection.getAttribute("class")).includes("section-visible"))
78+
79+
})
80+
81+
after(async function () {
82+
await teardown(this.driver, this, this.captureScreen);
83+
})
84+
})

selenium/test/pageobjects/BasePage.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,8 @@ module.exports = class BasePage {
131131
for (const index in optionList) {
132132
const t = await optionList[index].getText()
133133
const v = await optionList[index].getAttribute('value')
134-
table_model.push({"text":t, "value": v})
134+
const s = await optionList[index].getAttribute('selected')
135+
table_model.push({"text": t, "value": v, "selected" : s !== undefined})
135136
}
136137

137138
return table_model
@@ -300,13 +301,14 @@ module.exports = class BasePage {
300301

301302
async isDisplayed(locator) {
302303
try {
303-
let element = await driver.findElement(locator)
304+
let element = await this.driver.findElement(locator)
304305

305306
return this.driver.wait(until.elementIsVisible(element), this.timeout,
306307
'Timed out after [timeout=' + this.timeout + ';polling=' + this.polling + '] awaiting till visible ' + element,
307308
this.polling / 2)
308309
}catch(error) {
309-
return Promise.resolve(false)
310+
console.log("isDisplayed failed due to " + error);
311+
return Promise.resolve(false);
310312
}
311313
}
312314

selenium/test/pageobjects/SSOHomePage.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ module.exports = class SSOHomePage extends BasePage {
3737
async getLoginButton () {
3838
return this.getText(OAUTH2_LOGIN_BUTTON)
3939
}
40+
async getLoginButtonOnClick () {
41+
const element = await this.waitForDisplayed(OAUTH2_LOGIN_BUTTON);
42+
return element.getAttribute('onClick');
43+
}
4044
async getLogoutButton () {
4145
return this.getText(LOGOUT_BUTTON)
4246
}
@@ -74,6 +78,10 @@ module.exports = class SSOHomePage extends BasePage {
7478
async isOAuth2SectionVisible() {
7579
return this.isDisplayed(SECTION_LOGIN_WITH_OAUTH)
7680
}
81+
async isOAuth2SectionNotVisible() {
82+
return this.isElementNotVisible(SECTION_LOGIN_WITH_OAUTH)
83+
}
84+
7785
async getOAuth2Section() {
7886
return this.waitForDisplayed(SECTION_LOGIN_WITH_OAUTH)
7987
}

selenium/test/utils.js

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,9 @@ class CaptureScreenshot {
3939
const screenshotsSubDir = path.join(screenshotsDir, this.test)
4040
if (!fs.existsSync(screenshotsSubDir)) {
4141
await fsp.mkdir(screenshotsSubDir)
42-
}
42+
}
4343
const dest = path.join(screenshotsSubDir, name + '.png')
44+
console.log("screenshot saved to " + dest)
4445
await fsp.writeFile(dest, image, 'base64')
4546
}
4647
}
@@ -122,8 +123,31 @@ module.exports = {
122123
return d.driver.get(d.baseUrl)
123124
},
124125

125-
goToLogin: (d, token) => {
126-
return d.driver.get(d.baseUrl + '#/login?access_token=' + token)
126+
/**
127+
* For instance,
128+
* goToLogin(d, access_token, myAccessToken)
129+
* or
130+
* goToLogin(d, preferred_auth_mechanism, "oauth2:my-resource")
131+
*/
132+
goToLogin: (d, ...keyValuePairs) => {
133+
const params = [];
134+
for (let i = 0; i < keyValuePairs.length; i += 2) {
135+
const key = keyValuePairs[i];
136+
const value = keyValuePairs[i + 1];
137+
138+
if (key !== undefined) {
139+
// URL-encode both key and value
140+
const encodedKey = encodeURIComponent(key);
141+
const encodedValue = encodeURIComponent(value || '');
142+
params.push(`${encodedKey}=${encodedValue}`);
143+
}
144+
}
145+
// Build query string: "key1=value1&key2=value2"
146+
const queryString = params.join('&');
147+
148+
const url = d.baseUrl + '/login?' + queryString;
149+
console.log("Navigating to " + url);
150+
return d.driver.get(url);
127151
},
128152

129153
goToConnections: (d) => {
@@ -263,8 +287,15 @@ module.exports = {
263287
&& actualOption.text == expectedOptions[i].text))
264288
}
265289
},
290+
findOption: (value, options) => {
291+
for (let i = 0; i < options.length; i++) {
292+
if (options[i].value === value) return options[i];
293+
}
294+
return undefined;
295+
},
266296

267297
teardown: async (d, test, captureScreen = null) => {
298+
268299
driver = d.driver
269300
driver.manage().logs().get(logging.Type.BROWSER).then(function(entries) {
270301
entries.forEach(function(entry) {
@@ -274,8 +305,11 @@ module.exports = {
274305
if (test.currentTest) {
275306
if (test.currentTest.isPassed()) {
276307
driver.executeScript('lambda-status=passed')
277-
} else {
278-
if (captureScreen != null) await captureScreen.shot('after-failed')
308+
} else {
309+
if (captureScreen != null) {
310+
console.log("Teardown failed . capture...");
311+
await captureScreen.shot('after-failed');
312+
}
279313
driver.executeScript('lambda-status=failed')
280314
}
281315
}

0 commit comments

Comments
 (0)