]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-13427 Add renew token badge for admin
authorMathieu Suen <mathieu.suen@sonarsource.com>
Thu, 11 Nov 2021 17:04:34 +0000 (18:04 +0100)
committersonartech <sonartech@sonarsource.com>
Tue, 16 Nov 2021 20:03:55 +0000 (20:03 +0000)
server/sonar-web/src/main/js/api/project-badges.ts
server/sonar-web/src/main/js/app/components/nav/component/projectInformation/ProjectInformation.tsx
server/sonar-web/src/main/js/app/components/nav/component/projectInformation/__tests__/__snapshots__/ProjectInformation-test.tsx.snap
server/sonar-web/src/main/js/app/components/nav/component/projectInformation/badges/ProjectBadges.tsx
server/sonar-web/src/main/js/app/components/nav/component/projectInformation/badges/__tests__/ProjectBadges-test.tsx
server/sonar-web/src/main/js/app/components/nav/component/projectInformation/badges/__tests__/__snapshots__/ProjectBadges-test.tsx.snap
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 692b8d793c01458c07669b0bcde4de9569569405..b8783f72f9e3e60bcd0c7fa148ef41fe50a48458 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import throwGlobalError from '../app/utils/throwGlobalError';
-import { getJSON } from '../helpers/request';
+import { getJSON, postJSON } from '../helpers/request';
 
 export function getProjectBadgesToken(project: string) {
   return getJSON('/api/project_badges/token', { project })
     .then(({ token }) => token)
     .catch(throwGlobalError);
 }
+
+export function renewProjectBadgesToken(project: string) {
+  return postJSON('/api/project_badges/renew_token', { project }).catch(throwGlobalError);
+}
index 0b2072f2d116132a7b85ae4c8031e531f6a5f913..3df9c2b97f0233348c2829975b4e1d8bd716e9f7 100644 (file)
@@ -107,12 +107,7 @@ export class ProjectInformation extends React.PureComponent<Props, State> {
           <InfoDrawerPage
             displayed={page === ProjectInformationPages.badges}
             onPageChange={this.setPage}>
-            <ProjectBadges
-              branchLike={branchLike}
-              metrics={metrics}
-              project={component.key}
-              qualifier={component.qualifier}
-            />
+            <ProjectBadges branchLike={branchLike} metrics={metrics} component={component} />
           </InfoDrawerPage>
         )}
         {canConfigureNotifications && (
index fe5bdc421ce8076e4f1da711b92bded31e44fcf8..91b4f6e28fcab294c157b0694a84532631f76973 100644 (file)
@@ -35,6 +35,28 @@ exports[`should render correctly: default 1`] = `
     onPageChange={[Function]}
   >
     <ProjectBadges
+      component={
+        Object {
+          "breadcrumbs": Array [],
+          "key": "my-project",
+          "name": "MyProject",
+          "qualifier": "TRK",
+          "qualityGate": Object {
+            "isDefault": true,
+            "key": "30",
+            "name": "Sonar way",
+          },
+          "qualityProfiles": Array [
+            Object {
+              "deleted": false,
+              "key": "my-qp",
+              "language": "ts",
+              "name": "Sonar way",
+            },
+          ],
+          "tags": Array [],
+        }
+      }
       metrics={
         Object {
           "coverage": Object {
@@ -45,8 +67,6 @@ exports[`should render correctly: default 1`] = `
           },
         }
       }
-      project="my-project"
-      qualifier="TRK"
     />
   </InfoDrawerPage>
 </Fragment>
@@ -87,6 +107,28 @@ exports[`should render correctly: logged in user 1`] = `
     onPageChange={[Function]}
   >
     <ProjectBadges
+      component={
+        Object {
+          "breadcrumbs": Array [],
+          "key": "my-project",
+          "name": "MyProject",
+          "qualifier": "TRK",
+          "qualityGate": Object {
+            "isDefault": true,
+            "key": "30",
+            "name": "Sonar way",
+          },
+          "qualityProfiles": Array [
+            Object {
+              "deleted": false,
+              "key": "my-qp",
+              "language": "ts",
+              "name": "Sonar way",
+            },
+          ],
+          "tags": Array [],
+        }
+      }
       metrics={
         Object {
           "coverage": Object {
@@ -97,8 +139,6 @@ exports[`should render correctly: logged in user 1`] = `
           },
         }
       }
-      project="my-project"
-      qualifier="TRK"
     />
   </InfoDrawerPage>
   <InfoDrawerPage
@@ -182,6 +222,28 @@ exports[`should render correctly: measures loaded 1`] = `
     onPageChange={[Function]}
   >
     <ProjectBadges
+      component={
+        Object {
+          "breadcrumbs": Array [],
+          "key": "my-project",
+          "name": "MyProject",
+          "qualifier": "TRK",
+          "qualityGate": Object {
+            "isDefault": true,
+            "key": "30",
+            "name": "Sonar way",
+          },
+          "qualityProfiles": Array [
+            Object {
+              "deleted": false,
+              "key": "my-qp",
+              "language": "ts",
+              "name": "Sonar way",
+            },
+          ],
+          "tags": Array [],
+        }
+      }
       metrics={
         Object {
           "coverage": Object {
@@ -192,8 +254,6 @@ exports[`should render correctly: measures loaded 1`] = `
           },
         }
       }
-      project="my-project"
-      qualifier="TRK"
     />
   </InfoDrawerPage>
 </Fragment>
@@ -235,6 +295,29 @@ exports[`should render correctly: private 1`] = `
     onPageChange={[Function]}
   >
     <ProjectBadges
+      component={
+        Object {
+          "breadcrumbs": Array [],
+          "key": "my-project",
+          "name": "MyProject",
+          "qualifier": "TRK",
+          "qualityGate": Object {
+            "isDefault": true,
+            "key": "30",
+            "name": "Sonar way",
+          },
+          "qualityProfiles": Array [
+            Object {
+              "deleted": false,
+              "key": "my-qp",
+              "language": "ts",
+              "name": "Sonar way",
+            },
+          ],
+          "tags": Array [],
+          "visibility": "private",
+        }
+      }
       metrics={
         Object {
           "coverage": Object {
@@ -245,8 +328,6 @@ exports[`should render correctly: private 1`] = `
           },
         }
       }
-      project="my-project"
-      qualifier="TRK"
     />
   </InfoDrawerPage>
 </Fragment>
index 73bdf3fb9a1879a650ad81595a9fcc3d7c41a870..b219a077baea1299eefef52112c603ddc726a318 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
-import { getProjectBadgesToken } from '../../../../../../api/project-badges';
+import {
+  getProjectBadgesToken,
+  renewProjectBadgesToken
+} from '../../../../../../api/project-badges';
 import CodeSnippet from '../../../../../../components/common/CodeSnippet';
+import { Button } from '../../../../../../components/controls/buttons';
 import { Alert } from '../../../../../../components/ui/Alert';
+import DeferredSpinner from '../../../../../../components/ui/DeferredSpinner';
 import { getBranchLikeQuery } from '../../../../../../helpers/branch-like';
 import { translate } from '../../../../../../helpers/l10n';
 import { BranchLike } from '../../../../../../types/branch-like';
@@ -33,11 +38,11 @@ import { BadgeOptions, BadgeType, getBadgeSnippet, getBadgeUrl } from './utils';
 interface Props {
   branchLike?: BranchLike;
   metrics: T.Dict<T.Metric>;
-  project: string;
-  qualifier: string;
+  component: T.Component;
 }
 
 interface State {
+  isRenewing: boolean;
   token: string;
   selectedType: BadgeType;
   badgeOptions: BadgeOptions;
@@ -46,6 +51,7 @@ interface State {
 export default class ProjectBadges extends React.PureComponent<Props, State> {
   mounted = false;
   state: State = {
+    isRenewing: false,
     token: '',
     selectedType: BadgeType.measure,
     badgeOptions: { metric: MetricKey.alert_status }
@@ -61,8 +67,10 @@ export default class ProjectBadges extends React.PureComponent<Props, State> {
   }
 
   async fetchToken() {
-    const { project } = this.props;
-    const token = await getProjectBadgesToken(project);
+    const {
+      component: { key }
+    } = this.props;
+    const token = await getProjectBadgesToken(key).catch(() => '');
     if (this.mounted) {
       this.setState({ token });
     }
@@ -73,13 +81,36 @@ export default class ProjectBadges extends React.PureComponent<Props, State> {
   };
 
   handleUpdateOptions = (options: Partial<BadgeOptions>) => {
-    this.setState(state => ({ badgeOptions: { ...state.badgeOptions, ...options } }));
+    this.setState(state => ({
+      badgeOptions: { ...state.badgeOptions, ...options }
+    }));
+  };
+
+  handleRenew = async () => {
+    const {
+      component: { key }
+    } = this.props;
+
+    this.setState({ isRenewing: true });
+    await renewProjectBadgesToken(key).catch(() => {});
+    await this.fetchToken();
+    if (this.mounted) {
+      this.setState({ isRenewing: false });
+    }
   };
 
   render() {
-    const { branchLike, project, qualifier } = this.props;
-    const { selectedType, badgeOptions, token } = this.state;
-    const fullBadgeOptions = { project, ...badgeOptions, ...getBranchLikeQuery(branchLike) };
+    const {
+      branchLike,
+      component: { key: project, qualifier, configuration }
+    } = this.props;
+    const { isRenewing, selectedType, badgeOptions, token } = this.state;
+    const fullBadgeOptions = {
+      project,
+      ...badgeOptions,
+      ...getBranchLikeQuery(branchLike)
+    };
+    const canRenew = configuration?.showSettings;
 
     return (
       <div className="display-flex-column">
@@ -110,11 +141,31 @@ export default class ProjectBadges extends React.PureComponent<Props, State> {
           type={selectedType}
           updateOptions={this.handleUpdateOptions}
         />
-        <Alert variant="warning">{translate('overview.badges.leak_warning')}</Alert>
-        <CodeSnippet
-          isOneLine={true}
-          snippet={getBadgeSnippet(selectedType, fullBadgeOptions, token)}
-        />
+        {isRenewing ? (
+          <div className="spacer-top spacer-bottom display-flex-row display-flex-justify-center">
+            <DeferredSpinner className="spacer-top spacer-bottom" loading={isRenewing} />
+          </div>
+        ) : (
+          <CodeSnippet
+            isOneLine={true}
+            snippet={getBadgeSnippet(selectedType, fullBadgeOptions, token)}
+          />
+        )}
+
+        <Alert variant="warning">
+          <p>
+            {translate('overview.badges.leak_warning')}{' '}
+            {canRenew && translate('overview.badges.renew.description')}
+          </p>
+          {canRenew && (
+            <Button
+              disabled={isRenewing}
+              className="spacer-top it__project-info-renew-badge"
+              onClick={this.handleRenew}>
+              {translate('overview.badges.renew')}
+            </Button>
+          )}
+        </Alert>
       </div>
     );
   }
index 8b0868d437c4b000b62713947000cb1aee86ace6..da888216e20059f64710961350db6d9f64e8fa5d 100644 (file)
  */
 import { shallow } from 'enzyme';
 import * as React from 'react';
+import { getProjectBadgesToken } from '../../../../../../../api/project-badges';
+import CodeSnippet from '../../../../../../../components/common/CodeSnippet';
 import { mockBranch } from '../../../../../../../helpers/mocks/branch-like';
+import { mockComponent } from '../../../../../../../helpers/mocks/component';
 import { mockMetric } from '../../../../../../../helpers/testMocks';
 import { waitAndUpdate } from '../../../../../../../helpers/testUtils';
 import { Location } from '../../../../../../../helpers/urls';
+import { ComponentQualifier } from '../../../../../../../types/component';
 import { MetricKey } from '../../../../../../../types/metrics';
+import BadgeButton from '../BadgeButton';
 import ProjectBadges from '../ProjectBadges';
 
 jest.mock('../../../../../../../helpers/urls', () => ({
@@ -33,7 +38,8 @@ jest.mock('../../../../../../../helpers/urls', () => ({
 }));
 
 jest.mock('../../../../../../../api/project-badges', () => ({
-  getProjectBadgesToken: jest.fn().mockResolvedValue('foo')
+  getProjectBadgesToken: jest.fn().mockResolvedValue('foo'),
+  renewProjectBadgesToken: jest.fn().mockResolvedValue({})
 }));
 
 it('should display correctly', async () => {
@@ -42,6 +48,27 @@ it('should display correctly', async () => {
   expect(wrapper).toMatchSnapshot();
 });
 
+it('should renew token', async () => {
+  (getProjectBadgesToken as jest.Mock).mockResolvedValueOnce('foo').mockResolvedValueOnce('bar');
+  const wrapper = shallowRender({
+    component: mockComponent({ configuration: { showSettings: true } })
+  });
+  await waitAndUpdate(wrapper);
+  wrapper.find('.it__project-info-renew-badge').simulate('click');
+
+  // it shoud be loading
+  expect(wrapper.find('.it__project-info-renew-badge').props().disabled).toBe(true);
+
+  await waitAndUpdate(wrapper);
+  const buttons = wrapper.find(BadgeButton);
+  expect(buttons.at(0).props().url).toMatch('token=bar');
+  expect(buttons.at(1).props().url).toMatch('token=bar');
+  expect(wrapper.find(CodeSnippet).props().snippet).toMatch('token=bar');
+
+  // let's check that the loading has correclty ends.
+  expect(wrapper.find('.it__project-info-renew-badge').props().disabled).toBe(false);
+});
+
 function shallowRender(overrides = {}) {
   return shallow(
     <ProjectBadges
@@ -50,8 +77,7 @@ function shallowRender(overrides = {}) {
         [MetricKey.coverage]: mockMetric({ key: MetricKey.coverage }),
         [MetricKey.new_code_smells]: mockMetric({ key: MetricKey.new_code_smells })
       }}
-      project="foo"
-      qualifier="TRK"
+      component={mockComponent({ key: 'foo', qualifier: ComponentQualifier.Project })}
       {...overrides}
     />
   );
index 4c7f8b850db66be1d2c545613b0f43c339e3af9f..ea5049ad896937ee67b136f8b741ab5d582af5eb 100644 (file)
@@ -60,14 +60,17 @@ exports[`should display correctly 1`] = `
     type="measure"
     updateOptions={[Function]}
   />
-  <Alert
-    variant="warning"
-  >
-    overview.badges.leak_warning
-  </Alert>
   <CodeSnippet
     isOneLine={true}
     snippet="[![alert_status](host/api/project_badges/measure?branch=branch-6.7&project=foo&metric=alert_status&token=foo)](/dashboard)"
   />
+  <Alert
+    variant="warning"
+  >
+    <p>
+      overview.badges.leak_warning
+       
+    </p>
+  </Alert>
 </div>
 `;
index 423495025c3fc9c8540065305997f76869a0f153..4d4c5557a0cd447f7c1e3a452a4dd544c6253abd 100644 (file)
@@ -2997,7 +2997,7 @@ project_dump.pending_import=Import was scheduled on {0}, waiting to be processed
 project_dump.in_progress_import=Import is in progress, started {0}.
 project_dump.failed_import=The last import has failed. Please try once again.
 project_dump.import_form_description=A dump has been found on the file system for this project. You can import it by clicking on the button below.
-project_dump.import_form_description_disabled=Projects cannot be imported. This feature is only available starting from Enterprise Edition
+project_dump.import_form_description_disabled=Projects cannot be imported. This feature is only available starting from Enterprise Edition.
 
 #------------------------------------------------------------------------------
 #
@@ -3170,6 +3170,9 @@ overview.badges.quality_gate.description.APP=Displays the current quality gate s
 overview.badges.quality_gate.description.TRK=Displays the current quality gate status of your project.
 overview.badges.quality_gate.description.VW=Displays the current quality gate status of your portfolio.
 overview.badges.leak_warning=Project badges can expose your security rating and other measures. Only use project badges in trusted environments.
+overview.badges.renew=Renew Token
+overview.badges.renew.description=If your project badge security token has leaked to an unsafe environment, you can renew it:
+
 
 #------------------------------------------------------------------------------
 #