]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-11723 Prevent user update if authentication is delegated
authorWouter Admiraal <wouter.admiraal@sonarsource.com>
Mon, 24 Jun 2019 09:40:06 +0000 (11:40 +0200)
committersonartech <sonartech@sonarsource.com>
Fri, 28 Jun 2019 06:45:53 +0000 (08:45 +0200)
server/sonar-web/src/main/js/apps/users/components/UserForm.tsx
server/sonar-web/src/main/js/apps/users/components/__tests__/UserForm-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UserForm-test.tsx.snap [new file with mode: 0644]
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index bf795c87a4520fa798c1a5f332829129aa1044d7..d2c74a215f6a27d07e57625cbd14a688290d3f39 100644 (file)
@@ -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>
     );
   }
 }
diff --git a/server/sonar-web/src/main/js/apps/users/components/__tests__/UserForm-test.tsx b/server/sonar-web/src/main/js/apps/users/components/__tests__/UserForm-test.tsx
new file mode 100644 (file)
index 0000000..e8b487d
--- /dev/null
@@ -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} />
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UserForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UserForm-test.tsx.snap
new file mode 100644 (file)
index 0000000..8b21f61
--- /dev/null
@@ -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>
+`;
index 52d8534b76807df4e0ad77c34564d0c216a0e1c9..3a940ac84fe130d71e574ff92199d81889a9911c 100644 (file)
@@ -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