]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-20309 Do not sync GH permissions when admins did not explicitly consent to it
authorguillaume-peoch-sonarsource <guillaume.peoch@sonarsource.com>
Wed, 30 Aug 2023 16:07:04 +0000 (18:07 +0200)
committersonartech <sonartech@sonarsource.com>
Thu, 31 Aug 2023 20:02:57 +0000 (20:02 +0000)
server/sonar-web/src/main/js/api/mocks/AuthenticationServiceMock.ts
server/sonar-web/src/main/js/api/mocks/SettingsServiceMock.ts
server/sonar-web/src/main/js/apps/settings/components/authentication/Authentication.tsx
server/sonar-web/src/main/js/apps/settings/components/authentication/AutoProvisionningConsent.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/settings/components/authentication/GithubAuthenticationTab.tsx
server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-it.tsx
server/sonar-web/src/main/js/queries/settings.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 800cd0ef8a00ddfbe6047b621d28c26aecb7dd4e..c1f74f3d67e42be211ca346fe569c797fcf64cd6 100644 (file)
@@ -100,6 +100,11 @@ export default class AuthenticationServiceMock {
     };
   };
 
+  enableGithubProvisioning = () => {
+    this.scimStatus = false;
+    this.githubProvisioningStatus = true;
+  };
+
   handleActivateScim = () => {
     this.scimStatus = true;
     return Promise.resolve();
index 41ff5481689d3295e449a81d71f692a7b3c589cc..2505f1661a08573d1e53dbbd1930bb2fb0c7f9f3 100644 (file)
@@ -177,7 +177,11 @@ export default class SettingsServiceMock {
     const definition = this.#definitions.find(
       (d) => d.key === data.keys
     ) as ExtendedSettingDefinition;
-    if (definition.type === SettingType.PROPERTY_SET) {
+    if (data.keys === 'sonar.auth.github.userConsentForPermissionProvisioningRequired') {
+      this.#settingValues = this.#settingValues.filter(
+        (s) => s.key !== 'sonar.auth.github.userConsentForPermissionProvisioningRequired'
+      );
+    } else if (definition.type === SettingType.PROPERTY_SET) {
       setting.fieldValues = [];
     } else if (definition.multiValues === true) {
       setting.values = definition.defaultValue?.split(',') ?? [];
@@ -230,6 +234,16 @@ export default class SettingsServiceMock {
     this.#secretKeyAvailable = val;
   };
 
+  presetGithubAutoProvisioning = () => {
+    this.set('sonar.auth.github.enabled', 'true');
+    this.set('sonar.auth.github.clientId.secured', 'client ID');
+    this.set('sonar.auth.github.clientSecret.secured', 'shut');
+    this.set('sonar.auth.github.appId', 'Appid');
+    this.set('sonar.auth.github.privateKey.secured', 'private key');
+    this.set('sonar.auth.github.apiUrl', 'API url');
+    this.set('sonar.auth.github.webUrl', 'web URL');
+  };
+
   reset = () => {
     this.#settingValues = cloneDeep(this.#defaultValues);
     this.#definitions = cloneDeep(DEFAULT_DEFINITIONS_MOCK);
index 7d9b411e7aef209709795f8f1449d0c76756830a..f7e2e199427a43f9efc22f510f2110a85ac0e9b5 100644 (file)
@@ -174,6 +174,7 @@ export function Authentication(props: Props & WithAvailableFeaturesProps) {
 
                   {tab.key === AlmKeys.GitHub && (
                     <GithubAuthenticationTab
+                      currentTab={currentTab}
                       definitions={definitions.filter((def) => def.subCategory === AlmKeys.GitHub)}
                     />
                   )}
diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/AutoProvisionningConsent.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/AutoProvisionningConsent.tsx
new file mode 100644 (file)
index 0000000..16b932a
--- /dev/null
@@ -0,0 +1,93 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 { FormattedMessage } from 'react-intl';
+import DocLink from '../../../../components/common/DocLink';
+import Modal from '../../../../components/controls/Modal';
+import { Button } from '../../../../components/controls/buttons';
+import { translate } from '../../../../helpers/l10n';
+import { useToggleGithubProvisioningMutation } from '../../../../queries/identity-provider';
+import { useGetValueQuery, useResetSettingsMutation } from '../../../../queries/settings';
+
+const GITHUB_PERMISSION_USER_CONSENT =
+  'sonar.auth.github.userConsentForPermissionProvisioningRequired';
+
+export default function AutoProvisioningConsent() {
+  const toggleGithubProvisioning = useToggleGithubProvisioningMutation();
+  const resetSettingsMutation = useResetSettingsMutation();
+  const { data } = useGetValueQuery(GITHUB_PERMISSION_USER_CONSENT);
+
+  const header = translate('settings.authentication.github.confirm_auto_provisioning.header');
+
+  const removeConsentFlag = () => {
+    resetSettingsMutation.mutate([GITHUB_PERMISSION_USER_CONSENT]);
+  };
+
+  const switchToJIT = async () => {
+    await toggleGithubProvisioning.mutateAsync(false);
+    removeConsentFlag();
+  };
+
+  const continueWithAuto = async () => {
+    await toggleGithubProvisioning.mutateAsync(true);
+    removeConsentFlag();
+  };
+
+  if (data?.value !== '') {
+    return null;
+  }
+
+  return (
+    <Modal contentLabel={header} shouldCloseOnOverlayClick={false} size="medium">
+      <header className="modal-head">
+        <h2>{header}</h2>
+      </header>
+      <div className="modal-body">
+        <FormattedMessage
+          tagName="p"
+          id="settings.authentication.github.confirm_auto_provisioning.description1"
+        />
+        <FormattedMessage
+          id="settings.authentication.github.confirm_auto_provisioning.description2"
+          tagName="p"
+          values={{
+            documentation: (
+              <DocLink to="/instance-administration/authentication/github/">
+                <FormattedMessage id="documentation" />
+              </DocLink>
+            ),
+          }}
+        />
+        <FormattedMessage
+          tagName="p"
+          id="settings.authentication.github.confirm_auto_provisioning.question"
+        />
+      </div>
+      <footer className="modal-foot">
+        <Button onClick={continueWithAuto}>
+          {translate('settings.authentication.github.confirm_auto_provisioning.continue')}
+        </Button>
+        <Button onClick={switchToJIT}>
+          {translate('settings.authentication.github.confirm_auto_provisioning.switch_jit')}
+        </Button>
+      </footer>
+    </Modal>
+  );
+}
index 12f82b076470e01b5577c57db066692d6e301620..9421083b828cce0356a2bb8c267b4a96565b7fa6 100644 (file)
@@ -36,14 +36,16 @@ import {
 } from '../../../../queries/identity-provider';
 import { AlmKeys } from '../../../../types/alm-settings';
 import { ExtendedSettingDefinition } from '../../../../types/settings';
-import { DOCUMENTATION_LINK_SUFFIXES } from './Authentication';
+import { AuthenticationTabs, DOCUMENTATION_LINK_SUFFIXES } from './Authentication';
 import AuthenticationFormField from './AuthenticationFormField';
+import AutoProvisioningConsent from './AutoProvisionningConsent';
 import ConfigurationForm from './ConfigurationForm';
 import GitHubConfigurationValidity from './GitHubConfigurationValidity';
 import useGithubConfiguration, { GITHUB_JIT_FIELDS } from './hook/useGithubConfiguration';
 
 interface GithubAuthenticationProps {
   definitions: ExtendedSettingDefinition[];
+  currentTab: AuthenticationTabs;
 }
 
 const GITHUB_EXCLUDED_FIELD = [
@@ -53,7 +55,7 @@ const GITHUB_EXCLUDED_FIELD = [
 ];
 
 export default function GithubAuthenticationTab(props: GithubAuthenticationProps) {
-  const { definitions } = props;
+  const { definitions, currentTab } = props;
   const { data } = useIdentityProviderQuery();
   const [showEditModal, setShowEditModal] = useState(false);
   const [showConfirmProvisioningModal, setShowConfirmProvisioningModal] = useState(false);
@@ -341,6 +343,8 @@ export default function GithubAuthenticationTab(props: GithubAuthenticationProps
           hasLegacyConfiguration={hasLegacyConfiguration}
         />
       )}
+
+      {currentTab === AlmKeys.GitHub && <AutoProvisioningConsent />}
     </div>
   );
 }
index 2a09f47090b36d4fa4d15649f5ee1c0d5ad0d046..b0e2cea255a5804a2d9cb0c556ee21621e903d25 100644 (file)
@@ -185,6 +185,15 @@ const ui = {
     configDetailsDialog: byRole('dialog', {
       name: 'settings.authentication.github.configuration.validation.details.title',
     }),
+    continueAutoButton: byRole('button', {
+      name: 'settings.authentication.github.confirm_auto_provisioning.continue',
+    }),
+    switchJitButton: byRole('button', {
+      name: 'settings.authentication.github.confirm_auto_provisioning.switch_jit',
+    }),
+    consentDialog: byRole('dialog', {
+      name: 'settings.authentication.github.confirm_auto_provisioning.header',
+    }),
     getConfigDetailsTitle: () => within(ui.github.configDetailsDialog.get()).getByRole('heading'),
     getOrgs: () => within(ui.github.configDetailsDialog.get()).getAllByRole('listitem'),
     fillForm: async (user: UserEvent) => {
@@ -786,6 +795,38 @@ describe('Github tab', () => {
       expect(await github.syncWarning.find()).toBeInTheDocument();
       expect(github.syncSummary.get()).toBeInTheDocument();
     });
+
+    it('should display a modal if user was already using auto and continue using auto provisioning', async () => {
+      const user = userEvent.setup();
+      settingsHandler.presetGithubAutoProvisioning();
+      handler.enableGithubProvisioning();
+      settingsHandler.set('sonar.auth.github.userConsentForPermissionProvisioningRequired', '');
+      renderAuthentication([Feature.GithubProvisioning]);
+
+      await user.click(await github.tab.find());
+
+      expect(await github.consentDialog.find()).toBeInTheDocument();
+      await user.click(github.continueAutoButton.get());
+
+      expect(await github.githubProvisioningButton.find()).toBeChecked();
+      expect(github.consentDialog.query()).not.toBeInTheDocument();
+    });
+
+    it('should display a modal if user was already using auto and switch to JIT', async () => {
+      const user = userEvent.setup();
+      settingsHandler.presetGithubAutoProvisioning();
+      handler.enableGithubProvisioning();
+      settingsHandler.set('sonar.auth.github.userConsentForPermissionProvisioningRequired', '');
+      renderAuthentication([Feature.GithubProvisioning]);
+
+      await user.click(await github.tab.find());
+
+      expect(await github.consentDialog.find()).toBeInTheDocument();
+      await user.click(github.switchJitButton.get());
+
+      expect(await github.jitProvisioningButton.find()).toBeChecked();
+      expect(github.consentDialog.query()).not.toBeInTheDocument();
+    });
   });
 });
 
index 4aee8e6e1bbee86af3e97dc8825ec53621045265..2c6daabfb2505b352099f412deca2d665429fd19 100644 (file)
@@ -18,7 +18,7 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
-import { getValues, resetSettingValue, setSettingValue } from '../api/settings';
+import { getValue, getValues, resetSettingValue, setSettingValue } from '../api/settings';
 import { ExtendedSettingDefinition } from '../types/settings';
 
 type SettingValue = string | boolean | string[];
@@ -29,6 +29,12 @@ export function useGetValuesQuery(keys: string[]) {
   });
 }
 
+export function useGetValueQuery(key: string) {
+  return useQuery(['settings', 'details', key] as const, ({ queryKey: [_a, _b, key] }) => {
+    return getValue({ key }).then((v) => v ?? null);
+  });
+}
+
 export function useResetSettingsMutation() {
   const queryClient = useQueryClient();
   return useMutation({
index c3303f76e98e03ad38190417c99b1a47a48a9ccd..e78def3287548b6ec6dd77d7bd6a734ba72d4663 100644 (file)
@@ -1501,6 +1501,12 @@ settings.authentication.github.confirm.auto=Switch to automatic provisioning
 settings.authentication.github.confirm.jit=Switch to Just-in-Time provisioning
 settings.authentication.github.confirm.auto.description=Once you transition to automatic provisioning, groups, users, group memberships, and permissions on GitHub projects will be inherited from GitHub. You will no longer have the ability to edit them within SonarQube. Do you want to proceed with this change?
 settings.authentication.github.confirm.jit.description=Switching to Just-in-Time provisioning removes the automatic synchronization of users, groups, and group memberships. Users are provisioned and group memberships are updated only at user login. Are you sure?
+settings.authentication.github.confirm_auto_provisioning.header=Confirm the provisioning method
+settings.authentication.github.confirm_auto_provisioning.description1=Automatic user and group provisioning is currently suspended.
+settings.authentication.github.confirm_auto_provisioning.description2=This provisioning method has been enhanced. It now includes the synchronization of user permissions and project visibility from GitHub. For more details, please refer to the {documentation}.
+settings.authentication.github.confirm_auto_provisioning.question=Which provisioning method would you like to use?
+settings.authentication.github.confirm_auto_provisioning.continue=Automatic user, group, and permission provisioning
+settings.authentication.github.confirm_auto_provisioning.switch_jit=Just-in-Time user and group provisioning
 settings.authentication.github.configuration=GitHub Configuration
 settings.authentication.github.form.not_configured=GitHub App is not configured
 settings.authentication.github.form.legacy_configured=Compatibility with GitHub OAuth App is deprecated and will be removed in a future release. Your configuration will continue to work but with limited support. We recommend using GitHub Apps. Check out the {documentation} for more information.