diff options
author | Sarath Nair <91882341+sarath-nair-sonarsource@users.noreply.github.com> | 2024-07-25 09:25:53 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2024-07-25 20:02:50 +0000 |
commit | a78e7a23223b12d87209336c873866970f875f8f (patch) | |
tree | 5d57c10c36c6a88416b7c78188e5897cd3358ad0 | |
parent | 0fe1f68b3605c6132208034f4db57ba7b993739e (diff) | |
download | sonarqube-a78e7a23223b12d87209336c873866970f875f8f.tar.gz sonarqube-a78e7a23223b12d87209336c873866970f875f8f.zip |
SONAR-22592 Make GitLab's project visibility read only (#11415)
9 files changed, 225 insertions, 62 deletions
diff --git a/server/sonar-web/src/main/js/apps/permissions/project/components/PageHeader.tsx b/server/sonar-web/src/main/js/apps/permissions/project/components/PageHeader.tsx index d86d5c8fed5..a60cd6f400a 100644 --- a/server/sonar-web/src/main/js/apps/permissions/project/components/PageHeader.tsx +++ b/server/sonar-web/src/main/js/apps/permissions/project/components/PageHeader.tsx @@ -24,28 +24,36 @@ import * as React from 'react'; import { isPortfolioLike } from '~sonar-aligned/helpers/component'; import GitHubSynchronisationWarning from '../../../../app/components/GitHubSynchronisationWarning'; import { Image } from '../../../../components/common/Image'; -import { translate } from '../../../../helpers/l10n'; +import { translate, translateWithParameters } from '../../../../helpers/l10n'; import { isDefined } from '../../../../helpers/types'; +import { + useIsGitHubProjectQuery, + useIsGitLabProjectQuery, +} from '../../../../queries/devops-integration'; import { useGithubProvisioningEnabledQuery } from '../../../../queries/identity-provider/github'; +import { useGilabProvisioningEnabledQuery } from '../../../../queries/identity-provider/gitlab'; import { isApplication, isProject } from '../../../../types/component'; import { Component } from '../../../../types/types'; import ApplyTemplate from './ApplyTemplate'; interface Props { component: Component; - isGitHubProject?: boolean; loadHolders: () => void; } -export default function PageHeader(props: Props) { +export default function PageHeader(props: Readonly<Props>) { + const { component, loadHolders } = props; + const { configuration, key, qualifier, visibility } = component; const [applyTemplateModal, setApplyTemplateModal] = React.useState(false); + const { data: isGitHubProject } = useIsGitHubProjectQuery(key); + const { data: isGitLabProject } = useIsGitLabProjectQuery(key); const { data: githubProvisioningStatus } = useGithubProvisioningEnabledQuery(); + const { data: gitlabProvisioningStatus } = useGilabProvisioningEnabledQuery(); - const { component, isGitHubProject } = props; - const { configuration } = component; const provisionedByGitHub = isGitHubProject && !!githubProvisioningStatus; - const canApplyPermissionTemplate = - configuration?.canApplyPermissionTemplate && !provisionedByGitHub; + const provisionedByGitLab = isGitLabProject && !!gitlabProvisioningStatus; + const provisioned = provisionedByGitHub || provisionedByGitLab; + const canApplyPermissionTemplate = configuration?.canApplyPermissionTemplate && !provisioned; const handleApplyTemplate = () => { setApplyTemplateModal(true); @@ -56,15 +64,15 @@ export default function PageHeader(props: Props) { }; let description = translate('roles.page.description2'); - if (isPortfolioLike(component.qualifier)) { + if (isPortfolioLike(qualifier)) { description = translate('roles.page.description_portfolio'); - } else if (isApplication(component.qualifier)) { + } else if (isApplication(qualifier)) { description = translate('roles.page.description_application'); } const visibilityDescription = - isProject(component.qualifier) && component.visibility - ? translate('visibility', component.visibility, 'description', component.qualifier) + isProject(qualifier) && visibility + ? translate('visibility', visibility, 'description', qualifier) : undefined; return ( @@ -72,13 +80,16 @@ export default function PageHeader(props: Props) { <div> <Title> {translate('permissions.page')} - {provisionedByGitHub && ( + {provisioned && ( <Image - alt="github" + alt={provisionedByGitHub ? 'github' : 'gitlab'} className="sw-mx-2 sw-align-baseline" - aria-label={translate('project_permission.github_managed')} + aria-label={translateWithParameters( + 'project_permission.managed', + provisionedByGitHub ? translate('alm.github') : translate('alm.gitlab'), + )} height={16} - src="/images/alm/github.svg" + src={`/images/alm/${provisionedByGitHub ? 'github' : 'gitlab'}.svg`} /> )} </Title> @@ -86,11 +97,15 @@ export default function PageHeader(props: Props) { <div> <p>{description}</p> {isDefined(visibilityDescription) && <p>{visibilityDescription}</p>} - {provisionedByGitHub && ( + {provisioned && ( <> - <p>{translate('roles.page.description.github')}</p> + <p> + {provisionedByGitHub + ? translate('roles.page.description.github') + : translate('roles.page.description.gitlab')} + </p> <div className="sw-mt-2"> - <GitHubSynchronisationWarning short /> + {provisionedByGitHub && <GitHubSynchronisationWarning short />} </div> </> )} @@ -99,6 +114,11 @@ export default function PageHeader(props: Props) { {translate('project_permission.local_project_with_github_provisioning')} </FlagMessage> )} + {gitlabProvisioningStatus && !isGitLabProject && ( + <FlagMessage variant="warning" className="sw-mt-2"> + {translate('project_permission.local_project_with_gitlab_provisioning')} + </FlagMessage> + )} </div> </div> {canApplyPermissionTemplate && ( @@ -113,7 +133,7 @@ export default function PageHeader(props: Props) { {applyTemplateModal && ( <ApplyTemplate - onApply={props.loadHolders} + onApply={loadHolders} onClose={handleApplyTemplateClose} project={component} /> diff --git a/server/sonar-web/src/main/js/apps/permissions/project/components/PermissionsProjectApp.tsx b/server/sonar-web/src/main/js/apps/permissions/project/components/PermissionsProjectApp.tsx index dcfc3dbece6..c1e5708966a 100644 --- a/server/sonar-web/src/main/js/apps/permissions/project/components/PermissionsProjectApp.tsx +++ b/server/sonar-web/src/main/js/apps/permissions/project/components/PermissionsProjectApp.tsx @@ -24,22 +24,19 @@ import { Helmet } from 'react-helmet-async'; import { Visibility } from '~sonar-aligned/types/component'; import * as api from '../../../../api/permissions'; import withComponentContext from '../../../../app/components/componentContext/withComponentContext'; -import VisibilitySelector from '../../../../components/common/VisibilitySelector'; import AllHoldersList from '../../../../components/permissions/AllHoldersList'; import { FilterOption } from '../../../../components/permissions/SearchForm'; -import UseQuery from '../../../../helpers/UseQuery'; import { translate } from '../../../../helpers/l10n'; import { PERMISSIONS_ORDER_BY_QUALIFIER, convertToPermissionDefinitions, } from '../../../../helpers/permissions'; -import { useIsGitHubProjectQuery } from '../../../../queries/devops-integration'; -import { useGithubProvisioningEnabledQuery } from '../../../../queries/identity-provider/github'; import { ComponentContextShape } from '../../../../types/component'; import { Permissions } from '../../../../types/permissions'; import { Component, Paging, PermissionGroup, PermissionUser } from '../../../../types/types'; import '../../styles.css'; import PageHeader from './PageHeader'; +import PermissionsProjectVisibility from './PermissionsProjectVisibility'; import PublicProjectDisclaimer from './PublicProjectDisclaimer'; interface Props extends ComponentContextShape { @@ -336,8 +333,6 @@ class PermissionsProjectApp extends React.PureComponent<Props, State> { usersPaging, groupsPaging, } = this.state; - const canTurnToPrivate = - component.configuration && component.configuration.canUpdateProjectVisibilityToPrivate; let order = PERMISSIONS_ORDER_BY_QUALIFIER[component.qualifier]; if (component.visibility === Visibility.Public) { @@ -350,39 +345,22 @@ class PermissionsProjectApp extends React.PureComponent<Props, State> { <PageContentFontWrapper className="sw-my-8 sw-body-sm"> <Helmet defer={false} title={translate('permissions.page')} /> - <UseQuery query={useIsGitHubProjectQuery} args={[component.key]}> - {({ data: isGitHubProject }) => ( - <> - <PageHeader - component={component} - isGitHubProject={isGitHubProject} - loadHolders={this.loadHolders} - /> - <div> - <UseQuery query={useGithubProvisioningEnabledQuery}> - {({ data: githubProvisioningStatus, isFetching }) => ( - <VisibilitySelector - canTurnToPrivate={canTurnToPrivate} - className="sw-flex sw-my-4" - onChange={this.handleVisibilityChange} - loading={loading || isFetching} - disabled={isGitHubProject && !!githubProvisioningStatus} - visibility={component.visibility} - /> - )} - </UseQuery> - - {disclaimer && ( - <PublicProjectDisclaimer - component={component} - onClose={this.handleCloseDisclaimer} - onConfirm={this.handleTurnProjectToPublic} - /> - )} - </div> - </> + <PageHeader component={component} loadHolders={this.loadHolders} /> + <div> + <PermissionsProjectVisibility + component={component} + handleVisibilityChange={this.handleVisibilityChange} + isLoading={loading} + /> + + {disclaimer && ( + <PublicProjectDisclaimer + component={component} + onClose={this.handleCloseDisclaimer} + onConfirm={this.handleTurnProjectToPublic} + /> )} - </UseQuery> + </div> <AllHoldersList loading={loading} diff --git a/server/sonar-web/src/main/js/apps/permissions/project/components/PermissionsProjectVisibility.tsx b/server/sonar-web/src/main/js/apps/permissions/project/components/PermissionsProjectVisibility.tsx new file mode 100644 index 00000000000..8cde3d10676 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permissions/project/components/PermissionsProjectVisibility.tsx @@ -0,0 +1,62 @@ +/* + * SonarQube + * Copyright (C) 2009-2024 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 VisibilitySelector from '../../../../components/common/VisibilitySelector'; +import { + useIsGitHubProjectQuery, + useIsGitLabProjectQuery, +} from '../../../../queries/devops-integration'; +import { useGithubProvisioningEnabledQuery } from '../../../../queries/identity-provider/github'; +import { useGilabProvisioningEnabledQuery } from '../../../../queries/identity-provider/gitlab'; +import { Component } from '../../../../types/types'; + +interface Props { + component: Component; + handleVisibilityChange: (visibility: string) => void; + isLoading: boolean; +} + +export default function PermissionsProjectVisibility(props: Readonly<Props>) { + const { component, handleVisibilityChange, isLoading } = props; + const canTurnToPrivate = component.configuration?.canUpdateProjectVisibilityToPrivate; + + const { data: isGitHubProject } = useIsGitHubProjectQuery(component.key); + const { data: isGitLabProject } = useIsGitLabProjectQuery(component.key); + const { data: gitHubProvisioningStatus, isFetching: isFetchingGitHubProvisioningStatus } = + useGithubProvisioningEnabledQuery(); + const { data: gitLabProvisioningStatus, isFetching: isFetchingGitLabProvisioningStatus } = + useGilabProvisioningEnabledQuery(); + const isFetching = isFetchingGitHubProvisioningStatus || isFetchingGitLabProvisioningStatus; + const isDisabled = + (isGitHubProject && !!gitHubProvisioningStatus) || + (isGitLabProject && !!gitLabProvisioningStatus); + + return ( + <VisibilitySelector + canTurnToPrivate={canTurnToPrivate} + className="sw-flex sw-my-4" + onChange={handleVisibilityChange} + loading={isLoading || isFetching} + disabled={isDisabled} + visibility={component.visibility} + /> + ); +} diff --git a/server/sonar-web/src/main/js/apps/permissions/project/components/__tests__/PermissionsProject-it.tsx b/server/sonar-web/src/main/js/apps/permissions/project/components/__tests__/PermissionsProject-it.tsx index 24bad3963fb..8a190f4aa90 100644 --- a/server/sonar-web/src/main/js/apps/permissions/project/components/__tests__/PermissionsProject-it.tsx +++ b/server/sonar-web/src/main/js/apps/permissions/project/components/__tests__/PermissionsProject-it.tsx @@ -23,8 +23,10 @@ import { ComponentQualifier, Visibility } from '~sonar-aligned/types/component'; import AlmSettingsServiceMock from '../../../../../api/mocks/AlmSettingsServiceMock'; import DopTranslationServiceMock from '../../../../../api/mocks/DopTranslationServiceMock'; import GithubProvisioningServiceMock from '../../../../../api/mocks/GithubProvisioningServiceMock'; +import GitlabProvisioningServiceMock from '../../../../../api/mocks/GitlabProvisioningServiceMock'; import PermissionsServiceMock from '../../../../../api/mocks/PermissionsServiceMock'; import SystemServiceMock from '../../../../../api/mocks/SystemServiceMock'; +import { mockGitlabConfiguration } from '../../../../../helpers/mocks/alm-integrations'; import { mockComponent } from '../../../../../helpers/mocks/component'; import { mockGitHubConfiguration } from '../../../../../helpers/mocks/dop-translation'; import { mockPermissionGroup, mockPermissionUser } from '../../../../../helpers/mocks/permissions'; @@ -48,12 +50,14 @@ import { getPageObject } from '../../../test-utils'; let serviceMock: PermissionsServiceMock; let dopTranslationHandler: DopTranslationServiceMock; let githubHandler: GithubProvisioningServiceMock; +let gitlabHandler: GitlabProvisioningServiceMock; let almHandler: AlmSettingsServiceMock; let systemHandler: SystemServiceMock; beforeAll(() => { serviceMock = new PermissionsServiceMock(); dopTranslationHandler = new DopTranslationServiceMock(); githubHandler = new GithubProvisioningServiceMock(dopTranslationHandler); + gitlabHandler = new GitlabProvisioningServiceMock(); almHandler = new AlmSettingsServiceMock(); systemHandler = new SystemServiceMock(); }); @@ -62,6 +66,7 @@ afterEach(() => { serviceMock.reset(); dopTranslationHandler.reset(); githubHandler.reset(); + gitlabHandler.reset(); almHandler.reset(); }); @@ -330,7 +335,7 @@ describe('GH provisioning', () => { expect(ui.pageTitle.get()).toBeInTheDocument(); await waitFor(() => - expect(ui.pageTitle.get()).toHaveAccessibleName(/project_permission.github_managed/), + expect(ui.pageTitle.get()).toHaveAccessibleName(/project_permission.managed/), ); expect(ui.pageTitle.byRole('img').get()).toBeInTheDocument(); expect(ui.githubExplanations.get()).toBeInTheDocument(); @@ -437,6 +442,82 @@ describe('GH provisioning', () => { }); }); +describe('GL provisioning', () => { + beforeEach(() => { + systemHandler.setProvider(Provider.Gitlab); + }); + + it('should not allow to change visibility for GitLab Project with auto-provisioning', async () => { + const user = userEvent.setup(); + const ui = getPageObject(user); + dopTranslationHandler.gitHubConfigurations.push( + mockGitHubConfiguration({ provisioningType: ProvisioningType.jit }), + ); + gitlabHandler.setGitlabConfigurations([ + mockGitlabConfiguration({ id: '1', enabled: true, provisioningType: ProvisioningType.auto }), + ]); + almHandler.handleSetProjectBinding(AlmKeys.GitLab, { + almSetting: 'test', + repository: 'test', + monorepo: false, + project: 'my-project', + }); + + renderPermissionsProjectApp({}, { featureList: [Feature.GitlabProvisioning] }); + await ui.appLoaded(); + + expect(ui.visibilityRadio(Visibility.Public).get()).toBeDisabled(); + expect(ui.visibilityRadio(Visibility.Public).get()).toBeChecked(); + expect(ui.visibilityRadio(Visibility.Private).get()).toBeDisabled(); + await ui.turnProjectPrivate(); + expect(ui.visibilityRadio(Visibility.Private).get()).not.toBeChecked(); + }); + + it('should allow to change visibility for non-GitLab Project', async () => { + const user = userEvent.setup(); + const ui = getPageObject(user); + gitlabHandler.setGitlabConfigurations([ + mockGitlabConfiguration({ id: '1', enabled: true, provisioningType: ProvisioningType.auto }), + ]); + almHandler.handleSetProjectBinding(AlmKeys.GitHub, { + almSetting: 'test', + repository: 'test', + monorepo: false, + project: 'my-project', + }); + renderPermissionsProjectApp({}, { featureList: [Feature.GitlabProvisioning] }); + await ui.appLoaded(); + + expect(ui.visibilityRadio(Visibility.Public).get()).not.toHaveClass('disabled'); + expect(ui.visibilityRadio(Visibility.Public).get()).toBeChecked(); + expect(ui.visibilityRadio(Visibility.Private).get()).not.toHaveClass('disabled'); + await ui.turnProjectPrivate(); + expect(ui.visibilityRadio(Visibility.Private).get()).toBeChecked(); + }); + + it('should allow to change visibility for GitLab Project with disabled auto-provisioning', async () => { + const user = userEvent.setup(); + const ui = getPageObject(user); + gitlabHandler.setGitlabConfigurations([ + mockGitlabConfiguration({ id: '1', enabled: true, provisioningType: ProvisioningType.jit }), + ]); + almHandler.handleSetProjectBinding(AlmKeys.GitLab, { + almSetting: 'test', + repository: 'test', + monorepo: false, + project: 'my-project', + }); + renderPermissionsProjectApp({}, { featureList: [Feature.GitlabProvisioning] }); + await ui.appLoaded(); + + expect(ui.visibilityRadio(Visibility.Public).get()).not.toHaveClass('disabled'); + expect(ui.visibilityRadio(Visibility.Public).get()).toBeChecked(); + expect(ui.visibilityRadio(Visibility.Private).get()).not.toHaveClass('disabled'); + await ui.turnProjectPrivate(); + expect(ui.visibilityRadio(Visibility.Private).get()).toBeChecked(); + }); +}); + function renderPermissionsProjectApp( override: Partial<Component> = {}, contextOverride: Partial<RenderContext> = {}, diff --git a/server/sonar-web/src/main/js/components/common/VisibilitySelector.tsx b/server/sonar-web/src/main/js/components/common/VisibilitySelector.tsx index 9af36c10bb9..4aa0bb6a64f 100644 --- a/server/sonar-web/src/main/js/components/common/VisibilitySelector.tsx +++ b/server/sonar-web/src/main/js/components/common/VisibilitySelector.tsx @@ -33,7 +33,7 @@ export interface VisibilitySelectorProps { } export default function VisibilitySelector(props: Readonly<VisibilitySelectorProps>) { - const { className, canTurnToPrivate, visibility, disabled, loading = false } = props; + const { className, canTurnToPrivate, visibility, disabled, loading = false, onChange } = props; return ( <div className={classNames(className)}> <RadioButtonGroup @@ -46,7 +46,7 @@ export default function VisibilitySelector(props: Readonly<VisibilitySelectorPro isDisabled: (v === Visibility.Private && !canTurnToPrivate) || loading, }))} value={visibility} - onChange={props.onChange} + onChange={onChange} /> </div> ); diff --git a/server/sonar-web/src/main/js/queries/devops-integration.ts b/server/sonar-web/src/main/js/queries/devops-integration.ts index 344ba6c029a..ff7a6478ea2 100644 --- a/server/sonar-web/src/main/js/queries/devops-integration.ts +++ b/server/sonar-web/src/main/js/queries/devops-integration.ts @@ -79,6 +79,12 @@ export function useIsGitHubProjectQuery(project?: string) { }); } +export function useIsGitLabProjectQuery(project?: string) { + return useProjectBindingQuery<boolean>(project, { + select: (data) => data?.alm === AlmKeys.GitLab, + }); +} + export function useDeleteProjectAlmBindingMutation(project?: string) { const keyFromUrl = useProjectKeyFromLocation(); const client = useQueryClient(); diff --git a/server/sonar-web/src/main/js/queries/identity-provider/github.ts b/server/sonar-web/src/main/js/queries/identity-provider/github.ts index 556ad798e3e..dc09ffb9ea8 100644 --- a/server/sonar-web/src/main/js/queries/identity-provider/github.ts +++ b/server/sonar-web/src/main/js/queries/identity-provider/github.ts @@ -65,7 +65,9 @@ export function useGitHubSyncStatusQuery(options: GithubSyncStatusOptions = {}) export function useGithubProvisioningEnabledQuery() { const res = useGitHubSyncStatusQuery({ noRefetch: true }); - return mapReactQueryResult(res, (data) => data.enabled); + return mapReactQueryResult(res, (data) => { + return data.enabled; + }); } export function useSyncWithGitHubNow() { diff --git a/server/sonar-web/src/main/js/queries/identity-provider/gitlab.ts b/server/sonar-web/src/main/js/queries/identity-provider/gitlab.ts index 02ed747445c..eec78f438f4 100644 --- a/server/sonar-web/src/main/js/queries/identity-provider/gitlab.ts +++ b/server/sonar-web/src/main/js/queries/identity-provider/gitlab.ts @@ -28,6 +28,7 @@ import { updateGitLabConfiguration, } from '../../api/gitlab-provisioning'; import { translate } from '../../helpers/l10n'; +import { mapReactQueryResult } from '../../helpers/react-query'; import { AlmSyncStatus, ProvisioningType } from '../../types/provisioning'; import { TaskStatuses, TaskTypes } from '../../types/tasks'; @@ -99,6 +100,17 @@ export function useDeleteGitLabConfigurationMutation() { }); } +export function useGilabProvisioningEnabledQuery() { + const res = useGitLabConfigurationsQuery(); + + return mapReactQueryResult(res, (data) => + data.gitlabConfigurations?.some( + (configuration) => + configuration.enabled && configuration.provisioningType === ProvisioningType.auto, + ), + ); +} + export function useGitLabSyncStatusQuery() { const getLastSync = async () => { const lastSyncTasks = await getActivity({ diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index 75e52e56618..f0b6c46f7cb 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -628,9 +628,11 @@ roles.page.description2=Grant and revoke project-level permissions. Permissions roles.page.description_portfolio=Grant and revoke portfolio-level permissions. Permissions can be granted to groups or individual users. roles.page.description_application=Grant and revoke application-level permissions. Permissions can be granted to groups or individual users. roles.page.description.github=Project permissions are read-only for users provisioned from GitHub. For non-GitHub users, permissions can only be removed. +roles.page.description.gitlab=Project permissions are read-only for users provisioned from GitLab. For non-GitLab users, permissions can only be removed. roles.page.change_visibility=Change project visibility -project_permission.github_managed=Provisioned from GitHub +project_permission.managed=Provisioned from {0} project_permission.local_project_with_github_provisioning=Please note that this project is not linked to GitHub. Bind it to GitHub to benefit from permission provisioning. +project_permission.local_project_with_gitlab_provisioning=Please note that this project is not linked to GitLab. Bind it to GitLab to benefit from permission provisioning. project_permission.remove_only_confirmation=Are you sure you want to remove the permission {permission} from {holder}? The permission can not be added back. project_permission.remove_only_confirmation_title=Remove permission project_settings.page=General Settings |