Bladeren bron

SONAR-11723 Prevent user update if authentication is delegated

tags/8.0
Wouter Admiraal 5 jaren geleden
bovenliggende
commit
5df0a1d7f1

+ 108
- 101
server/sonar-web/src/main/js/apps/users/components/UserForm.tsx Bestand weergeven

@@ -22,16 +22,16 @@ import { uniq } from 'lodash';
import UserScmAccountInput from './UserScmAccountInput';
import { createUser, updateUser } from '../../../api/users';
import throwGlobalError from '../../../app/utils/throwGlobalError';
import Modal from '../../../components/controls/Modal';
import SimpleModal from '../../../components/controls/SimpleModal';
import { Button, ResetButtonLink, SubmitButton } from '../../../components/ui/buttons';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { parseError } from '../../../helpers/request';
import { Alert } from '../../../components/ui/Alert';

export interface Props {
user?: T.User;
onClose: () => void;
onUpdateUsers: () => void;
user?: T.User;
}

interface State {
@@ -102,8 +102,7 @@ export default class UserForm extends React.PureComponent<Props, State> {
handlePasswordChange = (event: React.SyntheticEvent<HTMLInputElement>) =>
this.setState({ password: event.currentTarget.value });

handleCreateUser = (event: React.SyntheticEvent<HTMLFormElement>) => {
event.preventDefault();
handleCreateUser = () => {
this.setState({ submitting: true });
createUser({
email: this.state.email || undefined,
@@ -117,8 +116,7 @@ export default class UserForm extends React.PureComponent<Props, State> {
}, this.handleError);
};

handleUpdateUser = (event: React.SyntheticEvent<HTMLFormElement>) => {
event.preventDefault();
handleUpdateUser = () => {
this.setState({ submitting: true });
updateUser({
email: this.state.email,
@@ -149,123 +147,132 @@ export default class UserForm extends React.PureComponent<Props, State> {

render() {
const { user } = this.props;
const { error, submitting } = this.state;
const { error } = this.state;

const header = user ? translate('users.update_user') : translate('users.create_user');
return (
<Modal contentLabel={header} onRequestClose={this.props.onClose} size="small">
<form
autoComplete="off"
id="user-form"
onSubmit={this.props.user ? this.handleUpdateUser : this.handleCreateUser}>
<header className="modal-head">
<h2>{header}</h2>
</header>
<SimpleModal
header={header}
onClose={this.props.onClose}
onSubmit={user ? this.handleUpdateUser : this.handleCreateUser}
size="small">
{({ onCloseClick, onFormSubmit, submitting }) => (
<form autoComplete="off" id="user-form" onSubmit={onFormSubmit}>
<header className="modal-head">
<h2>{header}</h2>
</header>

<div className="modal-body">
{error && <Alert variant="error">{error}</Alert>}

<div className="modal-body">
{error && <Alert variant="error">{error}</Alert>}
{!error && user && !user.local && (
<Alert variant="warning">{translate('users.cannot_update_delegated_user')}</Alert>
)}

{!user && (
{!user && (
<div className="modal-field">
<label htmlFor="create-user-login">
{translate('login')}
<em className="mandatory">*</em>
</label>
{/* keep this fake field to hack browser autofill */}
<input className="hidden" name="login-fake" type="text" />
<input
autoFocus={true}
id="create-user-login"
maxLength={255}
minLength={3}
name="login"
onChange={this.handleLoginChange}
required={true}
type="text"
value={this.state.login}
/>
<p className="note">{translateWithParameters('users.minimum_x_characters', 3)}</p>
</div>
)}
<div className="modal-field">
<label htmlFor="create-user-login">
{translate('login')}
<label htmlFor="create-user-name">
{translate('name')}
<em className="mandatory">*</em>
</label>
{/* keep this fake field to hack browser autofill */}
<input className="hidden" name="login-fake" type="text" />
<input className="hidden" name="name-fake" type="text" />
<input
autoFocus={true}
id="create-user-login"
maxLength={255}
minLength={3}
name="login"
onChange={this.handleLoginChange}
autoFocus={!!user}
disabled={user && !user.local}
id="create-user-name"
maxLength={200}
name="name"
onChange={this.handleNameChange}
required={true}
type="text"
value={this.state.login}
value={this.state.name}
/>
<p className="note">{translateWithParameters('users.minimum_x_characters', 3)}</p>
</div>
)}
<div className="modal-field">
<label htmlFor="create-user-name">
{translate('name')}
<em className="mandatory">*</em>
</label>
{/* keep this fake field to hack browser autofill */}
<input className="hidden" name="name-fake" type="text" />
<input
autoFocus={!!user}
id="create-user-name"
maxLength={200}
name="name"
onChange={this.handleNameChange}
required={true}
type="text"
value={this.state.name}
/>
</div>
<div className="modal-field">
<label htmlFor="create-user-email">{translate('users.email')}</label>
{/* keep this fake field to hack browser autofill */}
<input className="hidden" name="email-fake" type="email" />
<input
id="create-user-email"
maxLength={100}
name="email"
onChange={this.handleEmailChange}
type="email"
value={this.state.email}
/>
</div>
{!user && (
<div className="modal-field">
<label htmlFor="create-user-password">
{translate('password')}
<em className="mandatory">*</em>
</label>
<label htmlFor="create-user-email">{translate('users.email')}</label>
{/* keep this fake field to hack browser autofill */}
<input className="hidden" name="password-fake" type="password" />
<input className="hidden" name="email-fake" type="email" />
<input
id="create-user-password"
maxLength={50}
name="password"
onChange={this.handlePasswordChange}
required={true}
type="password"
value={this.state.password}
disabled={user && !user.local}
id="create-user-email"
maxLength={100}
name="email"
onChange={this.handleEmailChange}
type="email"
value={this.state.email}
/>
</div>
)}
<div className="modal-field">
<label>{translate('my_profile.scm_accounts')}</label>
{this.state.scmAccounts.map((scm, idx) => (
<UserScmAccountInput
idx={idx}
key={idx}
onChange={this.handleUpdateScmAccount}
onRemove={this.handleRemoveScmAccount}
scmAccount={scm}
/>
))}
<div className="spacer-bottom">
<Button className="js-scm-account-add" onClick={this.handleAddScmAccount}>
{translate('add_verb')}
</Button>
{!user && (
<div className="modal-field">
<label htmlFor="create-user-password">
{translate('password')}
<em className="mandatory">*</em>
</label>
{/* keep this fake field to hack browser autofill */}
<input className="hidden" name="password-fake" type="password" />
<input
id="create-user-password"
maxLength={50}
name="password"
onChange={this.handlePasswordChange}
required={true}
type="password"
value={this.state.password}
/>
</div>
)}
<div className="modal-field">
<label>{translate('my_profile.scm_accounts')}</label>
{this.state.scmAccounts.map((scm, idx) => (
<UserScmAccountInput
idx={idx}
key={idx}
onChange={this.handleUpdateScmAccount}
onRemove={this.handleRemoveScmAccount}
scmAccount={scm}
/>
))}
<div className="spacer-bottom">
<Button className="js-scm-account-add" onClick={this.handleAddScmAccount}>
{translate('add_verb')}
</Button>
</div>
<p className="note">{translate('user.login_or_email_used_as_scm_account')}</p>
</div>
<p className="note">{translate('user.login_or_email_used_as_scm_account')}</p>
</div>
</div>

<footer className="modal-foot">
{submitting && <i className="spinner spacer-right" />}
<SubmitButton disabled={submitting}>
{user ? translate('update_verb') : translate('create')}
</SubmitButton>
<ResetButtonLink onClick={this.props.onClose}>{translate('cancel')}</ResetButtonLink>
</footer>
</form>
</Modal>
<footer className="modal-foot">
{submitting && <i className="spinner spacer-right" />}
<SubmitButton disabled={submitting}>
{user ? translate('update_verb') : translate('create')}
</SubmitButton>
<ResetButtonLink onClick={onCloseClick}>{translate('cancel')}</ResetButtonLink>
</footer>
</form>
)}
</SimpleModal>
);
}
}

+ 110
- 0
server/sonar-web/src/main/js/apps/users/components/__tests__/UserForm-test.tsx Bestand weergeven

@@ -0,0 +1,110 @@
/*
* SonarQube
* Copyright (C) 2009-2019 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.
*/
import * as React from 'react';
import { shallow } from 'enzyme';
import UserForm from '../UserForm';
import { mockUser } from '../../../../helpers/testMocks';
import { createUser, updateUser } from '../../../../api/users';
import { waitAndUpdate, submit } from '../../../../helpers/testUtils';

jest.mock('../../../../api/users', () => ({
createUser: jest.fn().mockResolvedValue({}),
updateUser: jest.fn().mockResolvedValue({})
}));

it('should render correctly', () => {
expect(shallowRender().dive()).toMatchSnapshot();
expect(shallowRender({ user: undefined }).dive()).toMatchSnapshot();
});

it('should correctly show errors', async () => {
(updateUser as jest.Mock).mockRejectedValue({
response: {
status: 400
}
});
const wrapper = shallowRender();
submit(wrapper.dive().find('form'));
await waitAndUpdate(wrapper);
expect(
wrapper
.dive()
.find('Alert')
.dive()
.text()
).toMatch('default_error_message');
});

it('should correctly disable name and email fields for non-local users', () => {
const wrapper = shallowRender({ user: mockUser({ local: false }) }).dive();
expect(wrapper.find('#create-user-name').prop('disabled')).toBe(true);
expect(wrapper.find('#create-user-email').prop('disabled')).toBe(true);
expect(wrapper.find('Alert').exists()).toBe(true);
expect(
wrapper
.find('Alert')
.dive()
.text()
).toMatch('users.cannot_update_delegated_user');
});

it('should correctly create a new user', () => {
const email = 'foo@bar.ch';
const login = 'foo';
const name = 'Foo';
const password = 'bar';
const scmAccounts = ['gh', 'gh', 'bitbucket'];
const wrapper = shallowRender({ user: undefined });

wrapper.setState({ email, login, name, password, scmAccounts });

submit(wrapper.dive().find('form'));

expect(createUser).toBeCalledWith({
email,
login,
name,
password,
scmAccount: ['gh', 'bitbucket']
});
});

it('should correctly update a user', () => {
const email = 'foo@bar.ch';
const login = 'foo';
const name = 'Foo';
const scmAccounts = ['gh', 'gh', 'bitbucket'];
const wrapper = shallowRender({ user: mockUser({ email, login, name, scmAccounts }) }).dive();

submit(wrapper.find('form'));

expect(updateUser).toBeCalledWith({
email,
login,
name,
scmAccount: ['gh', 'bitbucket']
});
});

function shallowRender(props: Partial<UserForm['props']> = {}) {
return shallow(
<UserForm onClose={jest.fn()} onUpdateUsers={jest.fn()} user={mockUser()} {...props} />
);
}

+ 292
- 0
server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UserForm-test.tsx.snap Bestand weergeven

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

exports[`should render correctly 1`] = `
<Modal
contentLabel="users.update_user"
onRequestClose={[MockFunction]}
size="small"
>
<form
autoComplete="off"
id="user-form"
onSubmit={[Function]}
>
<header
className="modal-head"
>
<h2>
users.update_user
</h2>
</header>
<div
className="modal-body"
>
<div
className="modal-field"
>
<label
htmlFor="create-user-name"
>
name
<em
className="mandatory"
>
*
</em>
</label>
<input
className="hidden"
name="name-fake"
type="text"
/>
<input
autoFocus={true}
disabled={false}
id="create-user-name"
maxLength={200}
name="name"
onChange={[Function]}
required={true}
type="text"
value="John Doe"
/>
</div>
<div
className="modal-field"
>
<label
htmlFor="create-user-email"
>
users.email
</label>
<input
className="hidden"
name="email-fake"
type="email"
/>
<input
disabled={false}
id="create-user-email"
maxLength={100}
name="email"
onChange={[Function]}
type="email"
value=""
/>
</div>
<div
className="modal-field"
>
<label>
my_profile.scm_accounts
</label>
<div
className="spacer-bottom"
>
<Button
className="js-scm-account-add"
onClick={[Function]}
>
add_verb
</Button>
</div>
<p
className="note"
>
user.login_or_email_used_as_scm_account
</p>
</div>
</div>
<footer
className="modal-foot"
>
<SubmitButton
disabled={false}
>
update_verb
</SubmitButton>
<ResetButtonLink
onClick={[Function]}
>
cancel
</ResetButtonLink>
</footer>
</form>
</Modal>
`;

exports[`should render correctly 2`] = `
<Modal
contentLabel="users.create_user"
onRequestClose={[MockFunction]}
size="small"
>
<form
autoComplete="off"
id="user-form"
onSubmit={[Function]}
>
<header
className="modal-head"
>
<h2>
users.create_user
</h2>
</header>
<div
className="modal-body"
>
<div
className="modal-field"
>
<label
htmlFor="create-user-login"
>
login
<em
className="mandatory"
>
*
</em>
</label>
<input
className="hidden"
name="login-fake"
type="text"
/>
<input
autoFocus={true}
id="create-user-login"
maxLength={255}
minLength={3}
name="login"
onChange={[Function]}
required={true}
type="text"
value=""
/>
<p
className="note"
>
users.minimum_x_characters.3
</p>
</div>
<div
className="modal-field"
>
<label
htmlFor="create-user-name"
>
name
<em
className="mandatory"
>
*
</em>
</label>
<input
className="hidden"
name="name-fake"
type="text"
/>
<input
autoFocus={false}
id="create-user-name"
maxLength={200}
name="name"
onChange={[Function]}
required={true}
type="text"
value=""
/>
</div>
<div
className="modal-field"
>
<label
htmlFor="create-user-email"
>
users.email
</label>
<input
className="hidden"
name="email-fake"
type="email"
/>
<input
id="create-user-email"
maxLength={100}
name="email"
onChange={[Function]}
type="email"
value=""
/>
</div>
<div
className="modal-field"
>
<label
htmlFor="create-user-password"
>
password
<em
className="mandatory"
>
*
</em>
</label>
<input
className="hidden"
name="password-fake"
type="password"
/>
<input
id="create-user-password"
maxLength={50}
name="password"
onChange={[Function]}
required={true}
type="password"
value=""
/>
</div>
<div
className="modal-field"
>
<label>
my_profile.scm_accounts
</label>
<div
className="spacer-bottom"
>
<Button
className="js-scm-account-add"
onClick={[Function]}
>
add_verb
</Button>
</div>
<p
className="note"
>
user.login_or_email_used_as_scm_account
</p>
</div>
</div>
<footer
className="modal-foot"
>
<SubmitButton
disabled={false}
>
create
</SubmitButton>
<ResetButtonLink
onClick={[Function]}
>
cancel
</ResetButtonLink>
</footer>
</form>
</Modal>
`;

+ 1
- 0
sonar-core/src/main/resources/org/sonar/l10n/core.properties Bestand weergeven

@@ -3175,6 +3175,7 @@ users.deactivate_user=Deactivate User
users.deactivate_user.confirmation=Are you sure you want to deactivate "{0} ({1})"?
users.create_user=Create User
users.update_user=Update User
users.cannot_update_delegated_user=You cannot update the name and email of this user, as it is controlled by an external identity provider.
users.minimum_x_characters=Minimum {0} characters
users.email=Email
users.last_connection=Last connection

Laden…
Annuleren
Opslaan