]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-14606 Plugins require risk acknowledgment
authorJeremy Davis <jeremy.davis@sonarsource.com>
Wed, 24 Mar 2021 15:22:53 +0000 (16:22 +0100)
committersonartech <sonartech@sonarsource.com>
Thu, 15 Apr 2021 20:03:44 +0000 (20:03 +0000)
19 files changed:
server/sonar-web/src/main/js/app/components/indexation/IndexationNotification.tsx
server/sonar-web/src/main/js/apps/marketplace/App.tsx
server/sonar-web/src/main/js/apps/marketplace/AppContainer.tsx
server/sonar-web/src/main/js/apps/marketplace/__tests__/App-test.tsx
server/sonar-web/src/main/js/apps/marketplace/__tests__/AppContainer-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/marketplace/__tests__/__snapshots__/App-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/apps/marketplace/components/PluginRiskConsentBox.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/marketplace/components/__tests__/PluginRiskConsentBox-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/marketplace/components/__tests__/__snapshots__/PluginRiskConsentBox-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projects/components/ApplicationCreation.tsx
server/sonar-web/src/main/js/apps/projects/components/EmptyInstance.tsx
server/sonar-web/src/main/js/apps/projects/components/ProjectCreationMenu.tsx
server/sonar-web/src/main/js/apps/projects/components/__tests__/EmptyInstance-test.tsx
server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/EmptyInstance-test.tsx.snap
server/sonar-web/src/main/js/apps/projectsManagement/App.tsx
server/sonar-web/src/main/js/types/permissions.ts [new file with mode: 0644]
server/sonar-web/src/main/js/types/plugins.ts
server/sonar-web/src/main/js/types/settings.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 39a5b9b2d82d29255f8a3883f91bc1ae260c79ea..6111cb3bd0dc236570e6546cc469e3df82e1431f 100644 (file)
@@ -24,6 +24,7 @@ import withIndexationContext, {
 } from '../../../components/hoc/withIndexationContext';
 import { hasGlobalPermission, isLoggedIn } from '../../../helpers/users';
 import { IndexationNotificationType } from '../../../types/indexation';
+import { Permissions } from '../../../types/permissions';
 import './IndexationNotification.css';
 import IndexationNotificationHelper from './IndexationNotificationHelper';
 import IndexationNotificationRenderer from './IndexationNotificationRenderer';
@@ -44,7 +45,8 @@ export class IndexationNotification extends React.PureComponent<Props, State> {
     super(props);
 
     this.isSystemAdmin =
-      isLoggedIn(this.props.currentUser) && hasGlobalPermission(this.props.currentUser, 'admin');
+      isLoggedIn(this.props.currentUser) &&
+      hasGlobalPermission(this.props.currentUser, Permissions.Admin);
   }
 
   componentDidMount() {
index 9c0f15d14cc8f15600f12a94cf35fbdb07a90e85..6c837133eb2d65d171689059b0007d809d0cc86f 100644 (file)
@@ -20,6 +20,8 @@
 import { sortBy, uniqBy } from 'lodash';
 import * as React from 'react';
 import { Helmet } from 'react-helmet-async';
+import { FormattedMessage } from 'react-intl';
+import { Link } from 'react-router';
 import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner';
 import { translate } from 'sonar-ui-common/helpers/l10n';
 import {
@@ -28,10 +30,13 @@ import {
   getInstalledPluginsWithUpdates,
   getPluginUpdates
 } from '../../api/plugins';
+import { getValues, setSimpleSettingValue } from '../../api/settings';
 import Suggestions from '../../app/components/embed-docs-modal/Suggestions';
 import { Location, Router, withRouter } from '../../components/hoc/withRouter';
 import { EditionKey } from '../../types/editions';
-import { PendingPluginResult, Plugin } from '../../types/plugins';
+import { PendingPluginResult, Plugin, RiskConsent } from '../../types/plugins';
+import { SettingsKey } from '../../types/settings';
+import PluginRiskConsentBox from './components/PluginRiskConsentBox';
 import EditionBoxes from './EditionBoxes';
 import Footer from './Footer';
 import Header from './Header';
@@ -53,6 +58,7 @@ interface Props {
 interface State {
   loadingPlugins: boolean;
   plugins: Plugin[];
+  riskConsent?: RiskConsent;
 }
 
 export class App extends React.PureComponent<Props, State> {
@@ -62,6 +68,7 @@ export class App extends React.PureComponent<Props, State> {
   componentDidMount() {
     this.mounted = true;
     this.fetchQueryPlugins();
+    this.fetchRiskConsent();
   }
 
   componentDidUpdate(prevProps: Props) {
@@ -102,6 +109,27 @@ export class App extends React.PureComponent<Props, State> {
     );
   };
 
+  fetchRiskConsent = async () => {
+    const result = await getValues({ keys: SettingsKey.PluginRiskConsent });
+
+    if (!result || result.length < 1) {
+      return;
+    }
+
+    const [consent] = result;
+
+    this.setState({ riskConsent: consent.value as RiskConsent | undefined });
+  };
+
+  acknowledgeRisk = async () => {
+    await setSimpleSettingValue({
+      key: SettingsKey.PluginRiskConsent,
+      value: RiskConsent.Accepted
+    });
+
+    await this.fetchRiskConsent();
+  };
+
   updateQuery = (newQuery: Partial<Query>) => {
     const query = serializeQuery({ ...parseQuery(this.props.location.query), ...newQuery });
     this.props.router.push({ pathname: this.props.location.pathname, query });
@@ -115,7 +143,7 @@ export class App extends React.PureComponent<Props, State> {
 
   render() {
     const { currentEdition, standaloneMode, pendingPlugins } = this.props;
-    const { loadingPlugins, plugins } = this.state;
+    const { loadingPlugins, plugins, riskConsent } = this.state;
     const query = parseQuery(this.props.location.query);
     const filteredPlugins = filterPlugins(plugins, query.search);
 
@@ -128,9 +156,33 @@ export class App extends React.PureComponent<Props, State> {
         <header className="page-header">
           <h1 className="page-title">{translate('marketplace.page.plugins')}</h1>
           <div className="page-description">
-            {translate('marketplace.page.plugins.description')}
+            <p>{translate('marketplace.page.plugins.description')}</p>
+            {currentEdition !== EditionKey.community && (
+              <p className="spacer-top">
+                <FormattedMessage
+                  id="marketplace.page.plugins.description2"
+                  defaultMessage={translate('marketplace.page.plugins.description2')}
+                  values={{
+                    link: (
+                      <Link
+                        to="/documentation/instance-administration/marketplace/"
+                        target="_blank">
+                        {translate('marketplace.page.plugins.description2.link')}
+                      </Link>
+                    )
+                  }}
+                />
+              </p>
+            )}
           </div>
         </header>
+
+        <PluginRiskConsentBox
+          acknowledgeRisk={this.acknowledgeRisk}
+          currentEdition={currentEdition}
+          riskConsent={riskConsent}
+        />
+
         <Search
           query={query}
           updateCenterActive={this.props.updateCenterActive}
@@ -144,7 +196,7 @@ export class App extends React.PureComponent<Props, State> {
               <PluginsList
                 pending={pendingPlugins}
                 plugins={filteredPlugins}
-                readOnly={!standaloneMode}
+                readOnly={!standaloneMode || riskConsent !== RiskConsent.Accepted}
                 refreshPending={this.props.fetchPendingPlugins}
               />
               <Footer total={filteredPlugins.length} />
index 9a65f60dadb7720bee8aa00363295d12225e89f8..e49721d363f269dc756921cf1f80f5d18303e27e 100644 (file)
@@ -43,12 +43,14 @@ const mapStateToProps = (state: Store) => {
   };
 };
 
-const WithAdminContext = (props: StateToProps & OwnProps) => (
-  <AdminContext.Consumer>
-    {({ fetchPendingPlugins, pendingPlugins }) => (
-      <App fetchPendingPlugins={fetchPendingPlugins} pendingPlugins={pendingPlugins} {...props} />
-    )}
-  </AdminContext.Consumer>
-);
+function WithAdminContext(props: StateToProps & OwnProps) {
+  return (
+    <AdminContext.Consumer>
+      {({ fetchPendingPlugins, pendingPlugins }) => (
+        <App fetchPendingPlugins={fetchPendingPlugins} pendingPlugins={pendingPlugins} {...props} />
+      )}
+    </AdminContext.Consumer>
+  );
+}
 
 export default connect(mapStateToProps)(WithAdminContext);
index be40182dbb619a8eeca2350cb79ea6cd85851e86..e3e9e93bfd07040a67666db5a86d4f2e9bbebfe3 100644 (file)
@@ -26,7 +26,10 @@ import {
   getInstalledPluginsWithUpdates,
   getPluginUpdates
 } from '../../../api/plugins';
+import { getValues, setSimpleSettingValue } from '../../../api/settings';
 import { mockLocation, mockRouter } from '../../../helpers/testMocks';
+import { RiskConsent } from '../../../types/plugins';
+import { SettingsKey } from '../../../types/settings';
 import { App } from '../App';
 
 jest.mock('../../../api/plugins', () => {
@@ -40,6 +43,11 @@ jest.mock('../../../api/plugins', () => {
   };
 });
 
+jest.mock('../../../api/settings', () => ({
+  getValues: jest.fn().mockResolvedValue([]),
+  setSimpleSettingValue: jest.fn().mockResolvedValue(true)
+}));
+
 beforeEach(jest.clearAllMocks);
 
 it('should render correctly', async () => {
@@ -50,6 +58,25 @@ it('should render correctly', async () => {
   expect(wrapper).toMatchSnapshot('loaded');
 });
 
+it('should handle accepting the risk', async () => {
+  (getValues as jest.Mock)
+    .mockResolvedValueOnce([{ value: RiskConsent.NotAccepted }])
+    .mockResolvedValueOnce([{ value: RiskConsent.Accepted }]);
+
+  const wrapper = shallowRender();
+
+  await waitAndUpdate(wrapper);
+  expect(getValues).toBeCalledWith({ keys: SettingsKey.PluginRiskConsent });
+
+  wrapper.instance().acknowledgeRisk();
+
+  await new Promise(setImmediate);
+
+  expect(setSimpleSettingValue).toBeCalled();
+  expect(getValues).toBeCalledWith({ keys: SettingsKey.PluginRiskConsent });
+  expect(wrapper.state().riskConsent).toBe(RiskConsent.Accepted);
+});
+
 it('should fetch plugin info', async () => {
   const wrapper = shallowRender();
 
@@ -69,6 +96,7 @@ it('should fetch plugin info', async () => {
 function shallowRender(props: Partial<App['props']> = {}) {
   return shallow<App>(
     <App
+      currentEdition={EditionKey.developer}
       fetchPendingPlugins={jest.fn()}
       location={mockLocation()}
       pendingPlugins={{
diff --git a/server/sonar-web/src/main/js/apps/marketplace/__tests__/AppContainer-test.tsx b/server/sonar-web/src/main/js/apps/marketplace/__tests__/AppContainer-test.tsx
new file mode 100644 (file)
index 0000000..595ba21
--- /dev/null
@@ -0,0 +1,59 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 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 { connect } from 'react-redux';
+import { mockStore } from '../../../helpers/testMocks';
+import { getAppState, getGlobalSettingValue } from '../../../store/rootReducer';
+import { EditionKey } from '../../../types/editions';
+import '../AppContainer';
+
+jest.mock('react-redux', () => ({
+  connect: jest.fn(() => (a: any) => a)
+}));
+
+jest.mock('../../../store/rootReducer', () => {
+  return {
+    getAppState: jest.fn(),
+    getGlobalSettingValue: jest.fn()
+  };
+});
+
+describe('redux', () => {
+  it('should correctly map state and dispatch props', () => {
+    const store = mockStore();
+    const edition = EditionKey.developer;
+    const standalone = true;
+    const updateCenterActive = true;
+    (getAppState as jest.Mock).mockReturnValue({ edition, standalone });
+    (getGlobalSettingValue as jest.Mock).mockReturnValueOnce({
+      value: `${updateCenterActive}`
+    });
+
+    const [mapStateToProps] = (connect as jest.Mock).mock.calls[0];
+
+    const props = mapStateToProps(store);
+    expect(props).toEqual({
+      currentEdition: edition,
+      standaloneMode: standalone,
+      updateCenterActive
+    });
+
+    expect(getGlobalSettingValue).toHaveBeenCalledWith(store, 'sonar.updatecenter.activate');
+  });
+});
diff --git a/server/sonar-web/src/main/js/apps/marketplace/__tests__/__snapshots__/App-test.tsx.snap b/server/sonar-web/src/main/js/apps/marketplace/__tests__/__snapshots__/App-test.tsx.snap
deleted file mode 100644 (file)
index 09a6bc1..0000000
+++ /dev/null
@@ -1,116 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correctly: loaded 1`] = `
-<div
-  className="page page-limited"
-  id="marketplace-page"
->
-  <Suggestions
-    suggestions="marketplace"
-  />
-  <Helmet
-    defer={true}
-    encodeSpecialCharacters={true}
-    title="marketplace.page"
-  />
-  <Header />
-  <EditionBoxes />
-  <header
-    className="page-header"
-  >
-    <h1
-      className="page-title"
-    >
-      marketplace.page.plugins
-    </h1>
-    <div
-      className="page-description"
-    >
-      marketplace.page.plugins.description
-    </div>
-  </header>
-  <Search
-    query={
-      Object {
-        "filter": "all",
-        "search": "",
-      }
-    }
-    updateCenterActive={false}
-    updateQuery={[Function]}
-  />
-  <DeferredSpinner
-    loading={false}
-  >
-    <PluginsList
-      pending={
-        Object {
-          "installing": Array [],
-          "removing": Array [],
-          "updating": Array [],
-        }
-      }
-      plugins={
-        Array [
-          Object {
-            "key": "sonar-foo",
-            "name": "Sonar Foo",
-          },
-        ]
-      }
-      readOnly={true}
-      refreshPending={[MockFunction]}
-    />
-    <Footer
-      total={1}
-    />
-  </DeferredSpinner>
-</div>
-`;
-
-exports[`should render correctly: loading 1`] = `
-<div
-  className="page page-limited"
-  id="marketplace-page"
->
-  <Suggestions
-    suggestions="marketplace"
-  />
-  <Helmet
-    defer={true}
-    encodeSpecialCharacters={true}
-    title="marketplace.page"
-  />
-  <Header />
-  <EditionBoxes />
-  <header
-    className="page-header"
-  >
-    <h1
-      className="page-title"
-    >
-      marketplace.page.plugins
-    </h1>
-    <div
-      className="page-description"
-    >
-      marketplace.page.plugins.description
-    </div>
-  </header>
-  <Search
-    query={
-      Object {
-        "filter": "all",
-        "search": "",
-      }
-    }
-    updateCenterActive={false}
-    updateQuery={[Function]}
-  />
-  <DeferredSpinner
-    loading={true}
-  >
-    marketplace.plugin_list.no_plugins.all
-  </DeferredSpinner>
-</div>
-`;
diff --git a/server/sonar-web/src/main/js/apps/marketplace/components/PluginRiskConsentBox.tsx b/server/sonar-web/src/main/js/apps/marketplace/components/PluginRiskConsentBox.tsx
new file mode 100644 (file)
index 0000000..ed8cff3
--- /dev/null
@@ -0,0 +1,55 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 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 { Button } from 'sonar-ui-common/components/controls/buttons';
+import { translate } from 'sonar-ui-common/helpers/l10n';
+import { EditionKey } from '../../../types/editions';
+import { RiskConsent } from '../../../types/plugins';
+
+export interface PluginRiskConsentBoxProps {
+  acknowledgeRisk: () => void;
+  currentEdition?: EditionKey;
+  riskConsent?: RiskConsent;
+}
+
+export default function PluginRiskConsentBox(props: PluginRiskConsentBoxProps) {
+  const { currentEdition, riskConsent } = props;
+
+  if (riskConsent === RiskConsent.Accepted) {
+    return null;
+  }
+
+  return (
+    <div className="boxed-group it__plugin_risk_consent_box">
+      <h2>{translate('marketplace.risk_consent.title')}</h2>
+      <div className="boxed-group-inner">
+        <p>{translate('marketplace.risk_consent.description')}</p>
+        {currentEdition === EditionKey.community && (
+          <p className="spacer-top">{translate('marketplace.risk_consent.installation')}</p>
+        )}
+        <Button
+          className="display-block big-spacer-top button-primary"
+          onClick={props.acknowledgeRisk}>
+          {translate('marketplace.risk_consent.action')}
+        </Button>
+      </div>
+    </div>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/marketplace/components/__tests__/PluginRiskConsentBox-test.tsx b/server/sonar-web/src/main/js/apps/marketplace/components/__tests__/PluginRiskConsentBox-test.tsx
new file mode 100644 (file)
index 0000000..66f28bd
--- /dev/null
@@ -0,0 +1,39 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 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 { shallow } from 'enzyme';
+import * as React from 'react';
+import { EditionKey } from '../../../../types/editions';
+import { RiskConsent } from '../../../../types/plugins';
+import PluginRiskConsentBox, { PluginRiskConsentBoxProps } from '../PluginRiskConsentBox';
+
+it.each([[undefined], [RiskConsent.Accepted], [RiskConsent.NotAccepted], [RiskConsent.Required]])(
+  'should render correctly for risk consent %s',
+  (riskConsent?: RiskConsent) => {
+    expect(shallowRender({ riskConsent })).toMatchSnapshot();
+  }
+);
+
+it('should render correctly for community edition', () => {
+  expect(shallowRender({ currentEdition: EditionKey.community })).toMatchSnapshot();
+});
+
+function shallowRender(props: Partial<PluginRiskConsentBoxProps> = {}) {
+  return shallow(<PluginRiskConsentBox acknowledgeRisk={jest.fn()} {...props} />);
+}
diff --git a/server/sonar-web/src/main/js/apps/marketplace/components/__tests__/__snapshots__/PluginRiskConsentBox-test.tsx.snap b/server/sonar-web/src/main/js/apps/marketplace/components/__tests__/__snapshots__/PluginRiskConsentBox-test.tsx.snap
new file mode 100644 (file)
index 0000000..39b0f9e
--- /dev/null
@@ -0,0 +1,100 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly for community edition 1`] = `
+<div
+  className="boxed-group it__plugin_risk_consent_box"
+>
+  <h2>
+    marketplace.risk_consent.title
+  </h2>
+  <div
+    className="boxed-group-inner"
+  >
+    <p>
+      marketplace.risk_consent.description
+    </p>
+    <p
+      className="spacer-top"
+    >
+      marketplace.risk_consent.installation
+    </p>
+    <Button
+      className="display-block big-spacer-top button-primary"
+      onClick={[MockFunction]}
+    >
+      marketplace.risk_consent.action
+    </Button>
+  </div>
+</div>
+`;
+
+exports[`should render correctly for risk consent ACCEPTED 1`] = `""`;
+
+exports[`should render correctly for risk consent NOT_ACCEPTED 1`] = `
+<div
+  className="boxed-group it__plugin_risk_consent_box"
+>
+  <h2>
+    marketplace.risk_consent.title
+  </h2>
+  <div
+    className="boxed-group-inner"
+  >
+    <p>
+      marketplace.risk_consent.description
+    </p>
+    <Button
+      className="display-block big-spacer-top button-primary"
+      onClick={[MockFunction]}
+    >
+      marketplace.risk_consent.action
+    </Button>
+  </div>
+</div>
+`;
+
+exports[`should render correctly for risk consent REQUIRED 1`] = `
+<div
+  className="boxed-group it__plugin_risk_consent_box"
+>
+  <h2>
+    marketplace.risk_consent.title
+  </h2>
+  <div
+    className="boxed-group-inner"
+  >
+    <p>
+      marketplace.risk_consent.description
+    </p>
+    <Button
+      className="display-block big-spacer-top button-primary"
+      onClick={[MockFunction]}
+    >
+      marketplace.risk_consent.action
+    </Button>
+  </div>
+</div>
+`;
+
+exports[`should render correctly for risk consent undefined 1`] = `
+<div
+  className="boxed-group it__plugin_risk_consent_box"
+>
+  <h2>
+    marketplace.risk_consent.title
+  </h2>
+  <div
+    className="boxed-group-inner"
+  >
+    <p>
+      marketplace.risk_consent.description
+    </p>
+    <Button
+      className="display-block big-spacer-top button-primary"
+      onClick={[MockFunction]}
+    >
+      marketplace.risk_consent.action
+    </Button>
+  </div>
+</div>
+`;
index 12f4257256bf5a3ef49942f19348493a8188f8aa..56742d4e086557105461427c775bb2ca6211ddc2 100644 (file)
@@ -28,6 +28,7 @@ import { Router, withRouter } from '../../../components/hoc/withRouter';
 import { getComponentAdminUrl, getComponentOverviewUrl } from '../../../helpers/urls';
 import { hasGlobalPermission } from '../../../helpers/users';
 import { ComponentQualifier } from '../../../types/component';
+import { Permissions } from '../../../types/permissions';
 
 export interface ApplicationCreationProps {
   appState: Pick<T.AppState, 'qualifiers'>;
@@ -43,7 +44,7 @@ export function ApplicationCreation(props: ApplicationCreationProps) {
 
   const canCreateApplication =
     appState.qualifiers.includes(ComponentQualifier.Application) &&
-    hasGlobalPermission(currentUser, 'applicationcreator');
+    hasGlobalPermission(currentUser, Permissions.ApplicationCreation);
 
   if (!canCreateApplication) {
     return null;
index b9c251336bc195a822f9142923e15963bfd9fa84..34678061215cc6dbe2ede3c484e9ed94fee1989d 100644 (file)
@@ -23,6 +23,7 @@ import { Button } from 'sonar-ui-common/components/controls/buttons';
 import { translate } from 'sonar-ui-common/helpers/l10n';
 import { withRouter } from '../../../components/hoc/withRouter';
 import { hasGlobalPermission, isLoggedIn } from '../../../helpers/users';
+import { Permissions } from '../../../types/permissions';
 
 export interface EmptyInstanceProps {
   currentUser: T.CurrentUser;
@@ -32,7 +33,7 @@ export interface EmptyInstanceProps {
 export function EmptyInstance(props: EmptyInstanceProps) {
   const { currentUser, router } = props;
   const showNewProjectButton =
-    isLoggedIn(currentUser) && hasGlobalPermission(currentUser, 'provisioning');
+    isLoggedIn(currentUser) && hasGlobalPermission(currentUser, Permissions.ProjectCreation);
 
   return (
     <div className="projects-empty-list">
index fb282ba3f49a716e7192401e230d1b75480bec78..d2c52c76ebaf6e6eaf4982e2166f6c13cf6ad750 100644 (file)
@@ -27,6 +27,7 @@ import { withCurrentUser } from '../../../components/hoc/withCurrentUser';
 import { IMPORT_COMPATIBLE_ALMS } from '../../../helpers/constants';
 import { hasGlobalPermission } from '../../../helpers/users';
 import { AlmKeys, AlmSettingsInstance } from '../../../types/alm-settings';
+import { Permissions } from '../../../types/permissions';
 import ProjectCreationMenuItem from './ProjectCreationMenuItem';
 
 interface Props {
@@ -38,8 +39,6 @@ interface State {
   boundAlms: Array<string>;
 }
 
-const PROJECT_CREATION_PERMISSION = 'provisioning';
-
 const almSettingsValidators = {
   [AlmKeys.Azure]: (settings: AlmSettingsInstance) => !!settings.url,
   [AlmKeys.BitbucketServer]: (_: AlmSettingsInstance) => true,
@@ -68,7 +67,7 @@ export class ProjectCreationMenu extends React.PureComponent<Props, State> {
 
   fetchAlmBindings = async () => {
     const { currentUser } = this.props;
-    const canCreateProject = hasGlobalPermission(currentUser, PROJECT_CREATION_PERMISSION);
+    const canCreateProject = hasGlobalPermission(currentUser, Permissions.ProjectCreation);
 
     // getAlmSettings requires branchesEnabled
     if (!canCreateProject) {
@@ -94,7 +93,7 @@ export class ProjectCreationMenu extends React.PureComponent<Props, State> {
     const { className, currentUser } = this.props;
     const { boundAlms } = this.state;
 
-    const canCreateProject = hasGlobalPermission(currentUser, PROJECT_CREATION_PERMISSION);
+    const canCreateProject = hasGlobalPermission(currentUser, Permissions.ProjectCreation);
 
     if (!canCreateProject) {
       return null;
index ccd0eb4c0c60e9cff8c0ae9941a6b5d255f39855..3121467f77d9cd77a7ab5ae3ff83783ec4cb8646 100644 (file)
  */
 import { shallow } from 'enzyme';
 import * as React from 'react';
-import EmptyInstance from '../EmptyInstance';
+import { mockRouter } from '../../../../helpers/testMocks';
+import { EmptyInstance } from '../EmptyInstance';
 
 it('renders correctly for SQ', () => {
-  expect(shallow(<EmptyInstance currentUser={{ isLoggedIn: false }} />)).toMatchSnapshot();
+  expect(
+    shallow(<EmptyInstance currentUser={{ isLoggedIn: false }} router={mockRouter()} />)
+  ).toMatchSnapshot();
   expect(
     shallow(
       <EmptyInstance
         currentUser={{ isLoggedIn: true, permissions: { global: ['provisioning'] } }}
+        router={mockRouter()}
       />
     )
   ).toMatchSnapshot();
index 3c17a84451535edacf99a400e3e85a5427fe080a..247739f3cecd28dfa54b0ec8fe8c3a098c9da703 100644 (file)
@@ -1,26 +1,37 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
 exports[`renders correctly for SQ 1`] = `
-<EmptyInstance
-  currentUser={
-    Object {
-      "isLoggedIn": false,
-    }
-  }
-/>
+<div
+  className="projects-empty-list"
+>
+  <h3>
+    projects.no_projects.empty_instance
+  </h3>
+</div>
 `;
 
 exports[`renders correctly for SQ 2`] = `
-<EmptyInstance
-  currentUser={
-    Object {
-      "isLoggedIn": true,
-      "permissions": Object {
-        "global": Array [
-          "provisioning",
-        ],
-      },
-    }
-  }
-/>
+<div
+  className="projects-empty-list"
+>
+  <h3>
+    projects.no_projects.empty_instance.new_project
+  </h3>
+  <div>
+    <p
+      className="big-spacer-top"
+    >
+      projects.no_projects.empty_instance.how_to_add_projects
+    </p>
+    <p
+      className="big-spacer-top"
+    >
+      <Button
+        onClick={[Function]}
+      >
+        my_account.create_new.TRK
+      </Button>
+    </p>
+  </div>
+</div>
 `;
index 7c0abc8f42e0e888286e5accacf7d103b766278d..c10ecb3d411056e8ed0e221e29091af60a0d6a51 100644 (file)
@@ -30,6 +30,7 @@ import { getValues } from '../../api/settings';
 import Suggestions from '../../app/components/embed-docs-modal/Suggestions';
 import { hasGlobalPermission } from '../../helpers/users';
 import { getAppState, getCurrentUser, Store } from '../../store/rootReducer';
+import { Permissions } from '../../types/permissions';
 import { SettingsKey } from '../../types/settings';
 import CreateProjectForm from './CreateProjectForm';
 import Header from './Header';
@@ -205,7 +206,7 @@ export class App extends React.PureComponent<Props, State> {
 
         <Header
           defaultProjectVisibility={defaultProjectVisibility}
-          hasProvisionPermission={hasGlobalPermission(currentUser, 'provisioning')}
+          hasProvisionPermission={hasGlobalPermission(currentUser, Permissions.ProjectCreation)}
           onChangeDefaultProjectVisibility={this.handleDefaultProjectVisibilityChange}
           onProjectCreate={this.openCreateProjectForm}
         />
diff --git a/server/sonar-web/src/main/js/types/permissions.ts b/server/sonar-web/src/main/js/types/permissions.ts
new file mode 100644 (file)
index 0000000..2da4b7d
--- /dev/null
@@ -0,0 +1,25 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 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.
+ */
+
+export enum Permissions {
+  Admin = 'admin',
+  ProjectCreation = 'provisioning',
+  ApplicationCreation = 'applicationcreator'
+}
index f602016a7126cb7daf15be7b932168acf8d53785..cd4100e56e6e1a01cc70392e8f0bd3974574503e 100644 (file)
@@ -76,6 +76,12 @@ export enum PluginType {
   External = 'EXTERNAL'
 }
 
+export enum RiskConsent {
+  Accepted = 'ACCEPTED',
+  NotAccepted = 'NOT_ACCEPTED',
+  Required = 'REQUIRED'
+}
+
 export function isAvailablePlugin(plugin: Plugin): plugin is AvailablePlugin {
   return (plugin as any).release !== undefined;
 }
index c7ca6d250ab3eb405b25199a8068da02e88f90fb..0cf1661724bfdb6c7a9171cfa237a1d03dd23147 100644 (file)
@@ -20,7 +20,8 @@
 export const enum SettingsKey {
   DaysBeforeDeletingInactiveBranchesAndPRs = 'sonar.dbcleaner.daysBeforeDeletingInactiveBranchesAndPRs',
   DefaultProjectVisibility = 'projects.default.visibility',
-  ServerBaseUrl = 'sonar.core.serverBaseURL'
+  ServerBaseUrl = 'sonar.core.serverBaseURL',
+  PluginRiskConsent = 'sonar.plugins.risk.consent'
 }
 
 export type Setting = SettingValue & { definition: SettingDefinition };
index d548061fca2193b0637caab5f1fff6401d3912b1..f3d8ca19bf443fa5ffb11052ef432c7d3b456c18 100644 (file)
@@ -2699,7 +2699,9 @@ marketplace.page.you_are_running.developer=You are currently running a Developer
 marketplace.page.you_are_running.enterprise=You are currently running an Enterprise Edition.
 marketplace.page.you_are_running.datacenter=You are currently running a Data Center Edition.
 marketplace.page.plugins=Plugins
-marketplace.page.plugins.description=Plugins available in the MarketPlace are not provided or supported by SonarSource. Please reach out directly to their maintainers for support.
+marketplace.page.plugins.description=Plugins available in the Marketplace are not provided or supported by SonarSource. Please reach out directly to their maintainers for support.
+marketplace.page.plugins.description2=Installing a plugin is a manual operation. Please refer to the {link}.
+marketplace.page.plugins.description2.link=documentation
 marketplace.plugin_list.no_plugins.all=No installed plugins or updates available
 marketplace.plugin_list.no_plugins.installed=No installed plugins
 marketplace.plugin_list.no_plugins.updates=No plugin updates available
@@ -2747,6 +2749,17 @@ marketplace.release_notes=Release Notes
 marketplace.how_to_setup_cluster_url=Further configuration is required to set up a cluster. See {url} documentation.
 marketplace.search=Search by features, tags, or categories...
 
+marketplace.risk_consent.title=Installation of plugins
+marketplace.risk_consent.description=Plugins are not provided by SonarSource and are therefore installed at your own risk. SonarSource disclaims all liability for installing and using such plugins. 
+marketplace.risk_consent.installation=You can install plugins directly from the list below after you acknowledge the risk.
+marketplace.risk_consent.action=I understand the risk.
+
+plugin_risk_consent.title=Installation of plugins
+plugin_risk_consent.description=A third-party plugin has been detected.
+plugin_risk_consent.description2=Plugins are not provided by SonarSource and are therefore installed at your own risk. SonarSource disclaims all liability for installing and using such plugins.
+plugin_risk_consent.description3=If you wish to uninstall the plugin(s) instead, you may refer to the {link}.
+plugin_risk_consent.description3.link=documentation
+plugin_risk_consent.action=I understand the risk
 
 #------------------------------------------------------------------------------
 #
@@ -3961,6 +3974,8 @@ indexation.page_unavailable.title.portfolios=Portfolios page is temporarily unav
 indexation.page_unavailable.title={componentQualifier} {componentName} is temporarily unavailable
 indexation.page_unavailable.description=This page will be available after the data is reloaded. This might take a while depending on the amount of projects and issues in your SonarQube instance.
 indexation.page_unavailable.description.additional_information=You can keep analyzing your projects during this process.
+
+
 #------------------------------------------------------------------------------
 #
 # HOMEPAGE