Explorar el Código

SONAR-9357 Hide default login form if there are other ways to login (#2163)

tags/6.5-M2
Stas Vilchik hace 7 años
padre
commit
6ee2cedd71

+ 9
- 3
it/it-tests/src/test/java/it/ui/UiTest.java Ver fichero

@@ -22,14 +22,14 @@ package it.ui;
import com.sonar.orchestrator.Orchestrator;
import com.sonar.orchestrator.build.SonarScanner;
import it.Category4Suite;
import it.user.ForceAuthenticationTest;
import java.util.Map;
import org.junit.After;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.sonarqube.ws.client.GetRequest;
import org.sonarqube.ws.client.WsResponse;
import org.sonarqube.ws.client.setting.SetRequest;
import pageobjects.Navigation;
import util.ItUtils;

@@ -40,6 +40,7 @@ import static com.codeborne.selenide.Selenide.$;
import static com.codeborne.selenide.WebDriverRunner.url;
import static org.assertj.core.api.Assertions.assertThat;
import static util.ItUtils.projectDir;
import static util.ItUtils.resetSettings;
import static util.ItUtils.setServerProperty;

public class UiTest {
@@ -50,6 +51,12 @@ public class UiTest {
@Rule
public Navigation nav = Navigation.get(ORCHESTRATOR);

@Before
@After
public void resetData() throws Exception {
resetSettings(ORCHESTRATOR, null, "sonar.forceAuthentication");
}

@Test
public void footer_contains_information() {
nav.getFooter()
@@ -81,7 +88,6 @@ public class UiTest {
nav.getFooter()
.shouldNot(hasText("About"))
.shouldNot(hasText("Web API"));
setServerProperty(ORCHESTRATOR, "sonar.forceAuthentication", null);
}

@Test

+ 9
- 3
it/it-tests/src/test/java/it/user/BaseIdentityProviderTest.java Ver fichero

@@ -26,6 +26,7 @@ import it.Category4Suite;
import java.io.File;
import org.apache.commons.io.FileUtils;
import org.junit.After;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.Test;
@@ -77,12 +78,17 @@ public class BaseIdentityProviderTest {
adminWsClient = newAdminWsClient(ORCHESTRATOR);
}

@Before
@After
public void cleanUpUsersAndGroupsAndProperties() throws Exception {
public void resetData() throws Exception {
userRule.resetUsers();
userRule.removeGroups(GROUP1, GROUP2, GROUP3);
resetSettings(ORCHESTRATOR, null, "sonar.auth.fake-base-id-provider.enabled", "sonar.auth.fake-base-id-provider.user",
"sonar.auth.fake-base-id-provider.throwUnauthorizedMessage", "sonar.auth.fake-base-id-provider.enabledGroupsSync", "sonar.auth.fake-base-id-provider.groups",
resetSettings(ORCHESTRATOR, null,
"sonar.auth.fake-base-id-provider.enabled",
"sonar.auth.fake-base-id-provider.user",
"sonar.auth.fake-base-id-provider.throwUnauthorizedMessage",
"sonar.auth.fake-base-id-provider.enabledGroupsSync",
"sonar.auth.fake-base-id-provider.groups",
"sonar.auth.fake-base-id-provider.allowsUsersToSignUp");
}


+ 13
- 12
it/it-tests/src/test/java/it/user/OAuth2IdentityProviderTest.java Ver fichero

@@ -70,34 +70,35 @@ public class OAuth2IdentityProviderTest {
String fakeServerAuthProviderUrl;

@BeforeClass
public static void resetData() {
public static void initData() {
ORCHESTRATOR.resetData();
adminWsClient = newAdminWsClient(ORCHESTRATOR);
}

@After
public void resetUsers() throws Exception {
userRule.resetUsers();
}

@Before
public void setUp() throws Exception {
fakeServerAuthProvider = new MockWebServer();
fakeServerAuthProvider.start();
fakeServerAuthProviderUrl = fakeServerAuthProvider.url("").url().toString();
userRule.resetUsers();
resetSettings(ORCHESTRATOR, null, "sonar.auth.fake-oauth2-id-provider.enabled",
"sonar.auth.fake-oauth2-id-provider.url",
"sonar.auth.fake-oauth2-id-provider.user",
"sonar.auth.fake-oauth2-id-provider.throwUnauthorizedMessage",
"sonar.auth.fake-oauth2-id-provider.allowsUsersToSignUp");
resetData();
}

@After
public void tearDown() throws Exception {
resetData();
fakeServerAuthProvider.shutdown();
}

private void resetData(){
userRule.resetUsers();
resetSettings(ORCHESTRATOR, null,
"sonar.auth.fake-oauth2-id-provider.enabled",
"sonar.auth.fake-oauth2-id-provider.url",
"sonar.auth.fake-oauth2-id-provider.user",
"sonar.auth.fake-oauth2-id-provider.throwUnauthorizedMessage",
"sonar.auth.fake-oauth2-id-provider.allowsUsersToSignUp");
}

@Test
public void create_new_user_when_authenticate() throws Exception {
simulateRedirectionToCallback();

+ 20
- 6
server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.js Ver fichero

@@ -40,7 +40,8 @@ type Props = {
fetchMyOrganizations: () => Promise<*>,
location: Object,
organizations: Array<{ key: string, name: string }>,
router: { push: string => void }
router: { push: string => void },
sonarCloud: boolean
};

type State = {
@@ -156,11 +157,24 @@ export default class GlobalNavUser extends React.PureComponent {
}

renderAnonymous() {
return (
<li>
<a onClick={this.handleLogin} href="#">{translate('layout.login')}</a>
</li>
);
return this.props.sonarCloud
? <li>
<a href="/sessions/init/github">
<img
alt="GitHub"
className="navbar-global-login-github"
width="14"
height="14"
src="/static/authgithub/github.svg"
/>
{translate('layout.login')}
</a>
</li>
: <li>
<a onClick={this.handleLogin} href="#">
{translate('layout.login')}
</a>
</li>;
}

render() {

+ 2
- 1
server/sonar-web/src/main/js/app/utils/startReactApp.js Ver fichero

@@ -125,6 +125,7 @@ const startReactApp = () => {
<Redirect from="/profiles/index" to="/profiles" />
<Redirect from="/quality_gates/index" to="/quality_gates" />
<Redirect from="/settings/index" to="/settings" />
<Redirect from="/sessions/login" to="/sessions/new" />
<Redirect from="/system/index" to="/system" />

<Route path="markdown/help" component={MarkdownHelp} />
@@ -138,7 +139,7 @@ const startReactApp = () => {

<Route component={MigrationContainer}>
<Route component={SimpleSessionsContainer}>
<Route path="/sessions">{sessionsRoutes}</Route>
<Route path="/sessions" childRoutes={sessionsRoutes} />
</Route>

<Route path="/" component={App}>

+ 4
- 11
server/sonar-web/src/main/js/apps/about/components/AboutAppForSonarQubeDotCom.js Ver fichero

@@ -19,7 +19,6 @@
*/
// @flow
import React from 'react';
import { Link } from 'react-router';
import AboutProjects from './AboutProjects';
import EntryIssueTypesForSonarQubeDotCom from './EntryIssueTypesForSonarQubeDotCom';
import AboutRulesForSonarQubeDotCom from './AboutRulesForSonarQubeDotCom';
@@ -29,7 +28,6 @@ import AboutQualityGates from './AboutQualityGates';
import AboutLeakPeriod from './AboutLeakPeriod';
import AboutStandards from './AboutStandards';
import AboutScanners from './AboutScanners';
import { translate } from '../../../helpers/l10n';
import '../sonarqube-dot-com-styles.css';

type Props = {
@@ -57,16 +55,11 @@ export default function AboutAppForSonarQubeDotCom(props: Props) {
<h1 className="big-spacer-bottom">
Continuous Code Quality<br />as a Service
</h1>
<a
className="button button-active"
href="https://about.sonarcloud.io/get-started/"
target="_blank">
Get Started
</a>
{!props.currentUser.isLoggedIn &&
<Link to="/sessions/new" className="button big-spacer-left">
{translate('layout.login')}
</Link>}
<a className="sonarcloud-about-github-button" href="/sessions/init/github">
<img alt="GitHub" width="20" height="20" src="/static/authgithub/github.svg" />
Connect With GitHub to Get Started
</a>}
</div>

<div className="sqcom-about-page-instance">

+ 13
- 19
server/sonar-web/src/main/js/apps/about/sonarqube-dot-com-styles.css Ver fichero

@@ -31,35 +31,29 @@
font-weight: 300;
}

.sqcom-about-page-intro > .button {
.sonarcloud-about-github-button {
display: inline-block;
height: 44px;
line-height: 42px;
line-height: 46px;
padding-left: 20px;
padding-right: 20px;
border-color: #fff;
border: none;
border-radius: 3px;
color: #fff;
font-size: 16px;
background-color: #444;
color: #fff !important;
font-size: 15px;
font-weight: 500;
text-transform: uppercase;
transition: none;
}

.sqcom-about-page-intro > .button:hover {
background-color: #fff;
color: #4b9fd5;
}

.sqcom-about-page-intro > .button-active {
border-color: #b0eb41;
background-color: #b0eb41;
color: #225463;
.sonarcloud-about-github-button:hover,
.sonarcloud-about-github-button:focus {
background-color: #333;
}

.sqcom-about-page-intro > .button-active:hover {
border-color: #91d315;
background-color: #91d315;
color: #225463;
.sonarcloud-about-github-button img {
margin-top: 12px;
margin-right: 10px;
}

.sqcom-about-page-instance {

+ 81
- 49
server/sonar-web/src/main/js/apps/sessions/components/LoginForm.js Ver fichero

@@ -22,26 +22,49 @@ import React from 'react';
import GlobalMessagesContainer from '../../../app/components/GlobalMessagesContainer';
import { translate } from '../../../helpers/l10n';

type Props = {
identityProviders: Array<{
backgroundColor: string,
iconPath: string,
key: string,
name: string
}>,
onSubmit: (string, string) => void
};

type State = {
collapsed: boolean,
login: string,
password: string
};

export default class LoginForm extends React.PureComponent {
static propTypes = {
identityProviders: React.PropTypes.array.isRequired,
onSubmit: React.PropTypes.func.isRequired
};
props: Props;
state: State;

state = {
login: '',
password: ''
};
constructor(props: Props) {
super(props);
this.state = {
collapsed: props.identityProviders.length > 0,
login: '',
password: ''
};
}

handleSubmit = (e: Object) => {
e.preventDefault();
handleSubmit = (event: Event) => {
event.preventDefault();
this.props.onSubmit(this.state.login, this.state.password);
};

handleMoreOptionsClick = (event: Event) => {
event.preventDefault();
this.setState({ collapsed: false });
};

render() {
return (
<div>
<h1 className="maintenance-title text-center">Log In to SonarQube</h1>
<div id="login_form">
<h1 className="maintenance-title text-center">{translate('login.login_to_sonarqube')}</h1>

{this.props.identityProviders.length > 0 &&
<section className="oauth-providers">
@@ -65,46 +88,55 @@ export default class LoginForm extends React.PureComponent {
</ul>
</section>}

<form id="login_form" onSubmit={this.handleSubmit}>
<GlobalMessagesContainer />
{this.state.collapsed
? <div className="text-center">
<a
className="small text-muted js-more-options"
href="#"
onClick={this.handleMoreOptionsClick}>
{translate('login.more_options')}
</a>
</div>
: <form onSubmit={this.handleSubmit}>
<GlobalMessagesContainer />

<div className="big-spacer-bottom">
<label htmlFor="login" className="login-label">{translate('login')}</label>
<input
type="text"
id="login"
name="login"
className="login-input"
maxLength="255"
required={true}
autoFocus={true}
placeholder={translate('login')}
value={this.state.login}
onChange={e => this.setState({ login: e.target.value })}
/>
</div>
<div className="big-spacer-bottom">
<label htmlFor="login" className="login-label">{translate('login')}</label>
<input
type="text"
id="login"
name="login"
className="login-input"
maxLength="255"
required={true}
autoFocus={true}
placeholder={translate('login')}
value={this.state.login}
onChange={e => this.setState({ login: e.target.value })}
/>
</div>

<div className="big-spacer-bottom">
<label htmlFor="password" className="login-label">{translate('password')}</label>
<input
type="password"
id="password"
name="password"
className="login-input"
required={true}
placeholder={translate('password')}
value={this.state.password}
onChange={e => this.setState({ password: e.target.value })}
/>
</div>
<div className="big-spacer-bottom">
<label htmlFor="password" className="login-label">{translate('password')}</label>
<input
type="password"
id="password"
name="password"
className="login-input"
required={true}
placeholder={translate('password')}
value={this.state.password}
onChange={e => this.setState({ password: e.target.value })}
/>
</div>

<div>
<div className="text-right overflow-hidden">
<button name="commit" type="submit">{translate('sessions.log_in')}</button>
<a className="spacer-left" href={window.baseUrl + '/'}>{translate('cancel')}</a>
</div>
</div>
</form>
<div>
<div className="text-right overflow-hidden">
<button name="commit" type="submit">{translate('sessions.log_in')}</button>
<a className="spacer-left" href={window.baseUrl + '/'}>{translate('cancel')}</a>
</div>
</div>
</form>}
</div>
);
}

+ 60
- 0
server/sonar-web/src/main/js/apps/sessions/components/__tests__/LoginForm-test.js Ver fichero

@@ -0,0 +1,60 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import { shallow } from 'enzyme';
import LoginForm from '../LoginForm';
import { change, click, submit } from '../../../../helpers/testUtils';

const identityProvider = {
backgroundColor: '#000',
iconPath: '/some/path',
key: 'foo',
name: 'foo'
};

it('logs in with simple credentials', () => {
const onSubmit = jest.fn();
const wrapper = shallow(<LoginForm identityProviders={[]} onSubmit={onSubmit} />);
expect(wrapper).toMatchSnapshot();

change(wrapper.find('#login'), 'admin');
change(wrapper.find('#password'), 'admin');
submit(wrapper.find('form'));

expect(onSubmit).toBeCalledWith('admin', 'admin');
});

it('logs in with identity provider', () => {
const wrapper = shallow(
<LoginForm identityProviders={[identityProvider]} onSubmit={jest.fn()} />
);
expect(wrapper).toMatchSnapshot();
});

it('expands more options', () => {
const wrapper = shallow(
<LoginForm identityProviders={[identityProvider]} onSubmit={jest.fn()} />
);
expect(wrapper).toMatchSnapshot();

click(wrapper.find('.js-more-options'));
expect(wrapper).toMatchSnapshot();
});

+ 285
- 0
server/sonar-web/src/main/js/apps/sessions/components/__tests__/__snapshots__/LoginForm-test.js.snap Ver fichero

@@ -0,0 +1,285 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`expands more options 1`] = `
<div
id="login_form"
>
<h1
className="maintenance-title text-center"
>
login.login_to_sonarqube
</h1>
<section
className="oauth-providers"
>
<ul>
<li>
<a
href="/sessions/init/foo"
style={
Object {
"backgroundColor": "#000",
}
}
title="Log in with foo"
>
<img
alt="foo"
height="20"
src="/some/path"
width="20"
/>
<span>
Log in with
foo
</span>
</a>
</li>
</ul>
</section>
<div
className="text-center"
>
<a
className="small text-muted js-more-options"
href="#"
onClick={[Function]}
>
login.more_options
</a>
</div>
</div>
`;

exports[`expands more options 2`] = `
<div
id="login_form"
>
<h1
className="maintenance-title text-center"
>
login.login_to_sonarqube
</h1>
<section
className="oauth-providers"
>
<ul>
<li>
<a
href="/sessions/init/foo"
style={
Object {
"backgroundColor": "#000",
}
}
title="Log in with foo"
>
<img
alt="foo"
height="20"
src="/some/path"
width="20"
/>
<span>
Log in with
foo
</span>
</a>
</li>
</ul>
</section>
<form
onSubmit={[Function]}
>
<Connect(GlobalMessages) />
<div
className="big-spacer-bottom"
>
<label
className="login-label"
htmlFor="login"
>
login
</label>
<input
autoFocus={true}
className="login-input"
id="login"
maxLength="255"
name="login"
onChange={[Function]}
placeholder="login"
required={true}
type="text"
value=""
/>
</div>
<div
className="big-spacer-bottom"
>
<label
className="login-label"
htmlFor="password"
>
password
</label>
<input
className="login-input"
id="password"
name="password"
onChange={[Function]}
placeholder="password"
required={true}
type="password"
value=""
/>
</div>
<div>
<div
className="text-right overflow-hidden"
>
<button
name="commit"
type="submit"
>
sessions.log_in
</button>
<a
className="spacer-left"
href="/"
>
cancel
</a>
</div>
</div>
</form>
</div>
`;

exports[`logs in with identity provider 1`] = `
<div
id="login_form"
>
<h1
className="maintenance-title text-center"
>
login.login_to_sonarqube
</h1>
<section
className="oauth-providers"
>
<ul>
<li>
<a
href="/sessions/init/foo"
style={
Object {
"backgroundColor": "#000",
}
}
title="Log in with foo"
>
<img
alt="foo"
height="20"
src="/some/path"
width="20"
/>
<span>
Log in with
foo
</span>
</a>
</li>
</ul>
</section>
<div
className="text-center"
>
<a
className="small text-muted js-more-options"
href="#"
onClick={[Function]}
>
login.more_options
</a>
</div>
</div>
`;

exports[`logs in with simple credentials 1`] = `
<div
id="login_form"
>
<h1
className="maintenance-title text-center"
>
login.login_to_sonarqube
</h1>
<form
onSubmit={[Function]}
>
<Connect(GlobalMessages) />
<div
className="big-spacer-bottom"
>
<label
className="login-label"
htmlFor="login"
>
login
</label>
<input
autoFocus={true}
className="login-input"
id="login"
maxLength="255"
name="login"
onChange={[Function]}
placeholder="login"
required={true}
type="text"
value=""
/>
</div>
<div
className="big-spacer-bottom"
>
<label
className="login-label"
htmlFor="password"
>
password
</label>
<input
className="login-input"
id="password"
name="password"
onChange={[Function]}
placeholder="password"
required={true}
type="password"
value=""
/>
</div>
<div>
<div
className="text-right overflow-hidden"
>
<button
name="commit"
type="submit"
>
sessions.log_in
</button>
<a
className="spacer-left"
href="/"
>
cancel
</a>
</div>
</div>
</form>
</div>
`;

+ 27
- 11
server/sonar-web/src/main/js/apps/sessions/routes.js Ver fichero

@@ -17,15 +17,31 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import React from 'react';
import { Route, Redirect } from 'react-router';
import LoginFormContainer from './components/LoginFormContainer';
import Logout from './components/Logout';
import Unauthorized from './components/Unauthorized';

export default [
<Redirect key="login" from="/sessions/login" to="/sessions/new" />,
<Route key="new" path="new" component={LoginFormContainer} />,
<Route key="logout" path="logout" component={Logout} />,
<Route key="unauthorized" path="unauthorized" component={Unauthorized} />
const routes = [
{
path: 'new',
getComponent(_, callback) {
require.ensure([], require => {
callback(null, require('./components/LoginFormContainer').default);
});
}
},
{
path: 'logout',
getComponent(_, callback) {
require.ensure([], require => {
callback(null, require('./components/Logout').default);
});
}
},
{
path: 'unauthorized',
getComponent(_, callback) {
require.ensure([], require => {
callback(null, require('./components/Unauthorized').default);
});
}
}
];

export default routes;

+ 5
- 0
server/sonar-web/src/main/less/components/navbar.less Ver fichero

@@ -267,6 +267,11 @@
}
}

.navbar-global-login-github {
margin-top: 3px;
margin-right: 4px;
}


.navbar-context {
position: static;

+ 5
- 3
server/sonar-web/src/main/less/pages/login.less Ver fichero

@@ -70,9 +70,6 @@
}

.oauth-providers {
margin-bottom: 30px;
border-bottom: 1px solid @barBorderColor;

& > ul {
display: flex;
justify-content: space-around;
@@ -106,3 +103,8 @@
}
}
}

.oauth-providers + form {
padding-top: 30px;
border-top: 1px solid @barBorderColor;
}

+ 3
- 0
sonar-core/src/main/resources/org/sonar/l10n/core.properties Ver fichero

@@ -1841,6 +1841,9 @@ user.scm_account_already_used=The scm account '{0}' is already used by user(s) :
user.login_or_email_used_as_scm_account=Login and email are automatically considered as SCM accounts
user.password_cant_be_changed_on_external_auth=Password cannot be changed when external authentication is used

login.login_to_sonarqube=Log In to SonarQube
login.more_options=More options

#------------------------------------------------------------------------------
#
# USERS PAGE

Cargando…
Cancelar
Guardar