]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-15702 Prevent displaying App if no access to child projects
authorJeremy Davis <jeremy.davis@sonarsource.com>
Mon, 29 Nov 2021 17:29:47 +0000 (18:29 +0100)
committersonartech <sonartech@sonarsource.com>
Fri, 3 Dec 2021 20:03:33 +0000 (20:03 +0000)
server/sonar-web/src/main/js/app/components/NonAdminPagesContainer.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/__tests__/NonAdminPagesContainer-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/NonAdminPagesContainer-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/app/utils/startReactApp.tsx
sonar-core/src/main/resources/org/sonar/l10n/core.properties

diff --git a/server/sonar-web/src/main/js/app/components/NonAdminPagesContainer.tsx b/server/sonar-web/src/main/js/app/components/NonAdminPagesContainer.tsx
new file mode 100644 (file)
index 0000000..57d7e3a
--- /dev/null
@@ -0,0 +1,58 @@
+/*
+ * 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 { Alert } from '../../components/ui/Alert';
+import { translate } from '../../helpers/l10n';
+import { BranchLike } from '../../types/branch-like';
+import { isApplication } from '../../types/component';
+
+export interface NonAdminPagesContainerProps {
+  children: JSX.Element;
+  branchLike?: BranchLike;
+  branchLikes: BranchLike[];
+  component: T.Component;
+  isInProgress?: boolean;
+  isPending?: boolean;
+  onBranchesChange: () => void;
+  onComponentChange: (changes: {}) => void;
+}
+
+export default function NonAdminPagesContainer(props: NonAdminPagesContainerProps) {
+  const { children, component } = props;
+
+  /*
+   * Catch Applications for which the user does not have access to all child projects
+   * and prevent displaying whatever page was requested.
+   * This doesn't apply to admin pages (those are not within this container)
+   */
+  if (isApplication(component.qualifier) && !component.canBrowseAllChildProjects) {
+    return (
+      <div className="page page-limited display-flex-justify-center">
+        <Alert className="max-width-60 huge-spacer-top" display="block" variant="error">
+          <p>{translate('application.cannot_access_all_child_projects1')}</p>
+          <br />
+          <p>{translate('application.cannot_access_all_child_projects2')}</p>
+        </Alert>
+      </div>
+    );
+  }
+
+  return React.cloneElement(children, props);
+}
diff --git a/server/sonar-web/src/main/js/app/components/__tests__/NonAdminPagesContainer-test.tsx b/server/sonar-web/src/main/js/app/components/__tests__/NonAdminPagesContainer-test.tsx
new file mode 100644 (file)
index 0000000..d1acdc8
--- /dev/null
@@ -0,0 +1,69 @@
+/*
+ * 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 { mockComponent } from '../../../helpers/mocks/component';
+import { ComponentQualifier } from '../../../types/component';
+import NonAdminPagesContainer, { NonAdminPagesContainerProps } from '../NonAdminPagesContainer';
+
+function Child() {
+  return <div />;
+}
+
+it('should render correctly', () => {
+  expect(
+    shallowRender()
+      .find(Child)
+      .exists()
+  ).toBe(true);
+
+  expect(
+    shallowRender({
+      component: mockComponent({
+        qualifier: ComponentQualifier.Application,
+        canBrowseAllChildProjects: true
+      })
+    })
+      .find(Child)
+      .exists()
+  ).toBe(true);
+
+  const wrapper = shallowRender({
+    component: mockComponent({
+      qualifier: ComponentQualifier.Application
+    })
+  });
+
+  expect(wrapper.find(Child).exists()).toBe(false);
+  expect(wrapper).toMatchSnapshot();
+});
+
+function shallowRender(props: Partial<NonAdminPagesContainerProps> = {}) {
+  return shallow<NonAdminPagesContainerProps>(
+    <NonAdminPagesContainer
+      branchLikes={[]}
+      component={mockComponent()}
+      onBranchesChange={jest.fn()}
+      onComponentChange={jest.fn()}
+      {...props}>
+      <Child />
+    </NonAdminPagesContainer>
+  );
+}
diff --git a/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/NonAdminPagesContainer-test.tsx.snap b/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/NonAdminPagesContainer-test.tsx.snap
new file mode 100644 (file)
index 0000000..e3c581a
--- /dev/null
@@ -0,0 +1,21 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<div
+  className="page page-limited display-flex-justify-center"
+>
+  <Alert
+    className="max-width-60 huge-spacer-top"
+    display="block"
+    variant="error"
+  >
+    <p>
+      application.cannot_access_all_child_projects1
+    </p>
+    <br />
+    <p>
+      application.cannot_access_all_child_projects2
+    </p>
+  </Alert>
+</div>
+`;
index e5eaed2b30a32a246c3f525a7e6c141c3ae3e39d..1758c81ed316f6eefb90dc6be44a3817a87afb14 100644 (file)
@@ -67,6 +67,7 @@ import App from '../components/App';
 import GlobalContainer from '../components/GlobalContainer';
 import { PageContext } from '../components/indexation/PageUnavailableDueToIndexation';
 import MigrationContainer from '../components/MigrationContainer';
+import NonAdminPagesContainer from '../components/NonAdminPagesContainer';
 import getStore from './getStore';
 
 /*
@@ -156,46 +157,51 @@ function renderRedirects() {
 function renderComponentRoutes() {
   return (
     <Route component={lazyLoadComponent(() => import('../components/ComponentContainer'))}>
-      <RouteWithChildRoutes path="code" childRoutes={codeRoutes} />
-      <RouteWithChildRoutes path="component_measures" childRoutes={componentMeasuresRoutes} />
-      <RouteWithChildRoutes path="dashboard" childRoutes={overviewRoutes} />
-      <RouteWithChildRoutes path="portfolio" childRoutes={portfolioRoutes} />
-      <RouteWithChildRoutes path="project/activity" childRoutes={projectActivityRoutes} />
-      <Route
-        path="project/extension/:pluginKey/:extensionKey"
-        component={lazyLoadComponent(() => import('../components/extensions/ProjectPageExtension'))}
-      />
-      <Route
-        path="project/issues"
-        component={Issues}
-        onEnter={({ location: { query } }, replace) => {
-          if (query.types) {
-            if (query.types === 'SECURITY_HOTSPOT') {
-              replace({
-                pathname: '/security_hotspots',
-                query: { ...pick(query, ['id', 'branch', 'pullRequest']), assignedToMe: false }
-              });
-            } else {
-              query.types = query.types
-                .split(',')
-                .filter((type: string) => type !== 'SECURITY_HOTSPOT')
-                .join(',');
+      {/* This container is a catch-all for all non-admin pages */}
+      <Route component={NonAdminPagesContainer}>
+        <RouteWithChildRoutes path="code" childRoutes={codeRoutes} />
+        <RouteWithChildRoutes path="component_measures" childRoutes={componentMeasuresRoutes} />
+        <RouteWithChildRoutes path="dashboard" childRoutes={overviewRoutes} />
+        <RouteWithChildRoutes path="portfolio" childRoutes={portfolioRoutes} />
+        <RouteWithChildRoutes path="project/activity" childRoutes={projectActivityRoutes} />
+        <Route
+          path="project/extension/:pluginKey/:extensionKey"
+          component={lazyLoadComponent(() =>
+            import('../components/extensions/ProjectPageExtension')
+          )}
+        />
+        <Route
+          path="project/issues"
+          component={Issues}
+          onEnter={({ location: { query } }, replace) => {
+            if (query.types) {
+              if (query.types === 'SECURITY_HOTSPOT') {
+                replace({
+                  pathname: '/security_hotspots',
+                  query: { ...pick(query, ['id', 'branch', 'pullRequest']), assignedToMe: false }
+                });
+              } else {
+                query.types = query.types
+                  .split(',')
+                  .filter((type: string) => type !== 'SECURITY_HOTSPOT')
+                  .join(',');
+              }
             }
-          }
-        }}
-      />
-      <Route
-        path="security_hotspots"
-        component={lazyLoadComponent(() =>
-          import('../../apps/security-hotspots/SecurityHotspotsApp')
-        )}
-      />
-      <RouteWithChildRoutes path="project/quality_gate" childRoutes={projectQualityGateRoutes} />
-      <RouteWithChildRoutes
-        path="project/quality_profiles"
-        childRoutes={projectQualityProfilesRoutes}
-      />
-      <RouteWithChildRoutes path="tutorials" childRoutes={tutorialsRoutes} />
+          }}
+        />
+        <Route
+          path="security_hotspots"
+          component={lazyLoadComponent(() =>
+            import('../../apps/security-hotspots/SecurityHotspotsApp')
+          )}
+        />
+        <RouteWithChildRoutes path="project/quality_gate" childRoutes={projectQualityGateRoutes} />
+        <RouteWithChildRoutes
+          path="project/quality_profiles"
+          childRoutes={projectQualityProfilesRoutes}
+        />
+        <RouteWithChildRoutes path="tutorials" childRoutes={tutorialsRoutes} />
+      </Route>
       <Route component={lazyLoadComponent(() => import('../components/ProjectAdminContainer'))}>
         <Route
           path="project/admin/extension/:pluginKey/:extensionKey"
index aadb6a4b5b81a10fd92fe31dd0a0ad48927bfd28..90554804f2d385914e8203cd2366f0703cfaa137 100644 (file)
@@ -3979,6 +3979,14 @@ branch_like_navigation.only_one_branch.documentation=Branches documentation
 branch_like_navigation.only_one_branch.pr_analysis=Pull Request analysis
 branch_like_navigation.tutorial_for_ci=Show me how to set up my CI
 
+#------------------------------------------------------------------------------
+#
+# APPLICATIONS
+#
+#------------------------------------------------------------------------------
+application.cannot_access_all_child_projects1=You must have access to all the projects within this Application in order to browse it.
+application.cannot_access_all_child_projects2=Please contact your project administrator.
+
 #------------------------------------------------------------------------------
 #
 # PORTFOLIOS