From: Viktor Vorona Date: Fri, 14 Jul 2023 11:53:58 +0000 (+0200) Subject: SONAR-19790 Disallow applying permission template on managed project X-Git-Tag: 10.2.0.77647~385 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=683b7ad836fda87fb6e35388389dc9df07b8aeba;p=sonarqube.git SONAR-19790 Disallow applying permission template on managed project --- diff --git a/server/sonar-web/src/main/js/api/mocks/ProjectsManagementServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/ProjectsManagementServiceMock.ts index 29e2e0e515f..b1e89751259 100644 --- a/server/sonar-web/src/main/js/api/mocks/ProjectsManagementServiceMock.ts +++ b/server/sonar-web/src/main/js/api/mocks/ProjectsManagementServiceMock.ts @@ -38,7 +38,7 @@ const defaultProject = [ mockProject({ key: 'project1', name: 'Project 1' }), mockProject({ key: 'project2', name: 'Project 2', visibility: Visibility.Private }), mockProject({ key: 'project3', name: 'Project 3', lastAnalysisDate: undefined }), - mockProject({ key: 'projectProvisioned', name: 'Project 4' }), + mockProject({ key: 'projectProvisioned', name: 'Project 4', managed: true }), mockProject({ key: 'portfolio1', name: 'Portfolio 1', qualifier: ComponentQualifier.Portfolio }), mockProject({ key: 'portfolio2', diff --git a/server/sonar-web/src/main/js/api/project-management.ts b/server/sonar-web/src/main/js/api/project-management.ts index c560ff1e8c3..c12831d1ace 100644 --- a/server/sonar-web/src/main/js/api/project-management.ts +++ b/server/sonar-web/src/main/js/api/project-management.ts @@ -43,6 +43,7 @@ export interface ProjectBase { export interface Project extends ProjectBase { lastAnalysisDate?: string; + managed?: boolean; } export interface SearchProjectsParameters extends BaseSearchProjectsParameters { diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/ProjectManagementApp.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/ProjectManagementApp.tsx index 780a6a16775..73b876c80d7 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/ProjectManagementApp.tsx +++ b/server/sonar-web/src/main/js/apps/projectsManagement/ProjectManagementApp.tsx @@ -188,7 +188,9 @@ class ProjectManagementApp extends React.PureComponent { }; onAllSelected = () => { - this.setState(({ projects }) => ({ selection: projects.map((project) => project.key) })); + this.setState(({ projects }) => ({ + selection: projects.filter((p) => !p.managed).map((project) => project.key), + })); }; onAllDeselected = () => { diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.tsx index 026f6cac3cb..9a4c207e465 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.tsx +++ b/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.tsx @@ -25,8 +25,9 @@ import Checkbox from '../../components/controls/Checkbox'; import Tooltip from '../../components/controls/Tooltip'; import QualifierIcon from '../../components/icons/QualifierIcon'; import DateFormatter from '../../components/intl/DateFormatter'; -import { translateWithParameters } from '../../helpers/l10n'; +import { translate, translateWithParameters } from '../../helpers/l10n'; import { getComponentOverviewUrl } from '../../helpers/urls'; +import { ComponentQualifier } from '../../types/component'; import { LoggedInUser } from '../../types/users'; import './ProjectRow.css'; import ProjectRowActions from './ProjectRowActions'; @@ -38,59 +39,61 @@ interface Props { selected: boolean; } -export default class ProjectRow extends React.PureComponent { - handleProjectCheck = (checked: boolean) => { - this.props.onProjectCheck(this.props.project, checked); - }; +export default function ProjectRow(props: Props) { + const { currentUser, project, selected } = props; - render() { - const { project, selected } = this.props; + const handleProjectCheck = (checked: boolean) => { + props.onProjectCheck(project, checked); + }; - return ( - - - - + return ( + + + + - - - + + + - - {project.name} - - - + + {project.name} + + + {project.qualifier === ComponentQualifier.Project && !project.managed && ( + {translate('local')} + )} + - - - + + + - - - {project.key} - - + + + {project.key} + + - - {project.lastAnalysisDate ? ( - - ) : ( - — - )} - + + {project.lastAnalysisDate ? ( + + ) : ( + — + )} + - - - - - ); - } + + + + + ); } diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRowActions.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRowActions.tsx index b41ac29b0b3..fa9064adf54 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRowActions.tsx +++ b/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRowActions.tsx @@ -17,7 +17,7 @@ * 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 React, { useState } from 'react'; import { getComponentNavigation } from '../../api/navigation'; import { Project } from '../../api/project-management'; import ActionsDropdown, { ActionsDropdownItem } from '../../components/controls/ActionsDropdown'; @@ -33,129 +33,93 @@ export interface Props { project: Project; } -interface State { - applyTemplateModal: boolean; - hasAccess?: boolean; - loading: boolean; - restoreAccessModal: boolean; -} - -export default class ProjectRowActions extends React.PureComponent { - mounted = false; - state: State = { applyTemplateModal: false, loading: false, restoreAccessModal: false }; - - componentDidMount() { - this.mounted = true; - } +export default function ProjectRowActions({ currentUser, project }: Props) { + const [applyTemplateModal, setApplyTemplateModal] = useState(false); + const [hasAccess, setHasAccess] = useState(undefined); + const [loading, setLoading] = useState(false); + const [restoreAccessModal, setRestoreAccessModal] = useState(false); - componentWillUnmount() { - this.mounted = false; - } - - fetchPermissions = () => { - this.setState({ loading: true }); - getComponentNavigation({ component: this.props.project.key }).then( + const fetchPermissions = () => { + setLoading(true); + getComponentNavigation({ component: project.key }).then( ({ configuration }) => { - if (this.mounted) { - const hasAccess = Boolean( - configuration && configuration.showPermissions && configuration.canBrowseProject - ); - this.setState({ hasAccess, loading: false }); - } + const hasAccess = Boolean( + configuration && configuration.showPermissions && configuration.canBrowseProject + ); + setHasAccess(hasAccess); + setLoading(false); }, () => { - if (this.mounted) { - this.setState({ loading: false }); - } + setLoading(false); } ); }; - handleDropdownOpen = () => { - if (this.state.hasAccess === undefined && !this.state.loading) { - this.fetchPermissions(); - } - }; - - handleApplyTemplateClick = () => { - this.setState({ applyTemplateModal: true }); - }; - - handleApplyTemplateClose = () => { - if (this.mounted) { - this.setState({ applyTemplateModal: false }); + const handleDropdownOpen = () => { + if (hasAccess === undefined && !loading) { + fetchPermissions(); } }; - handleRestoreAccessClick = () => { - this.setState({ restoreAccessModal: true }); - }; - - handleRestoreAccessClose = () => this.setState({ restoreAccessModal: false }); - - handleRestoreAccessDone = () => { - this.setState({ hasAccess: true, restoreAccessModal: false }); + const handleRestoreAccessDone = () => { + setRestoreAccessModal(false); + setHasAccess(true); }; - render() { - const { hasAccess, loading } = this.state; - - return ( - <> - - {loading ? ( - - - - ) : ( - <> - {hasAccess === true && ( - - {translate('edit_permissions')} - - )} - - {hasAccess === false && ( - - {translate('global_permissions.restore_access')} - - )} - - )} + return ( + <> + + {loading ? ( + + + + ) : ( + <> + {hasAccess === true && ( + + {translate(project.managed ? 'show_permissions' : 'edit_permissions')} + + )} + + {hasAccess === false && !project.managed && ( + setRestoreAccessModal(true)} + > + {translate('global_permissions.restore_access')} + + )} + + )} + {!project.managed && ( setApplyTemplateModal(true)} > {translate('projects_role.apply_template')} - - - {this.state.restoreAccessModal && ( - )} - - {this.state.applyTemplateModal && ( - - )} - - ); - } + + + {restoreAccessModal && ( + setRestoreAccessModal(false)} + onRestoreAccess={handleRestoreAccessDone} + project={project} + /> + )} + + {applyTemplateModal && ( + setApplyTemplateModal(false)} project={project} /> + )} + + ); } diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectManagementApp-it.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectManagementApp-it.tsx index 195c37e350d..f4631ddc7db 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectManagementApp-it.tsx +++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectManagementApp-it.tsx @@ -17,7 +17,7 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { screen, waitFor, within } from '@testing-library/react'; +import { act, screen, waitFor, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import selectEvent from 'react-select-event'; import AuthenticationServiceMock from '../../../api/mocks/AuthenticationServiceMock'; @@ -73,7 +73,9 @@ const ui = { firstProjectActions: byRole('button', { name: 'projects_management.show_actions_for_x.Project 1', }), + projectActions: byRole('button', { name: /projects_management.show_actions_for_x/ }), editPermissions: byRole('link', { name: 'edit_permissions' }), + showPermissions: byRole('link', { name: 'show_permissions' }), applyPermissionTemplate: byRole('button', { name: 'projects_role.apply_template' }), restoreAccess: byRole('button', { name: 'global_permissions.restore_access' }), editPermissionsPage: byText('/project_roles?id=project1'), @@ -365,13 +367,13 @@ it('should create project', async () => { expect(ui.successMsg.get(dialog)).toBeInTheDocument(); await user.click(ui.close.get(dialog)); expect(ui.row.getAll()).toHaveLength(6); - expect(ui.row.getAll()[1]).toHaveTextContent('qualifier.TRKa Testvisibility.privatetest—'); + expect(ui.row.getAll()[1]).toHaveTextContent('qualifier.TRKa Testlocalvisibility.privatetest—'); }); it('should edit permissions of single project', async () => { const user = userEvent.setup(); renderProjectManagementApp(); - await user.click(await ui.firstProjectActions.find()); + await act(async () => user.click(await ui.firstProjectActions.find())); expect(ui.restoreAccess.query()).not.toBeInTheDocument(); expect(ui.editPermissions.get()).toBeInTheDocument(); await user.click(ui.editPermissions.get()); @@ -382,7 +384,7 @@ it('should edit permissions of single project', async () => { it('should apply template for single object', async () => { const user = userEvent.setup(); renderProjectManagementApp(); - await user.click(await ui.firstProjectActions.find()); + await act(async () => user.click(await ui.firstProjectActions.find())); await user.click(ui.applyPermissionTemplate.get()); expect(ui.applyTemplateDialog.get()).toBeInTheDocument(); @@ -400,14 +402,14 @@ it('should apply template for single object', async () => { it('should restore access to admin', async () => { const user = userEvent.setup(); renderProjectManagementApp({}, { login: 'gooduser2' }); - await user.click(await ui.firstProjectActions.find()); + await act(async () => user.click(await ui.firstProjectActions.find())); expect(await ui.restoreAccess.find()).toBeInTheDocument(); expect(ui.editPermissions.query()).not.toBeInTheDocument(); await user.click(ui.restoreAccess.get()); expect(ui.restoreAccessDialog.get()).toBeInTheDocument(); - await user.click(ui.restore.get(ui.restoreAccessDialog.get())); + await act(() => user.click(ui.restore.get(ui.restoreAccessDialog.get()))); expect(ui.restoreAccessDialog.query()).not.toBeInTheDocument(); - await user.click(await ui.firstProjectActions.find()); + await act(async () => user.click(await ui.firstProjectActions.find())); expect(ui.restoreAccess.query()).not.toBeInTheDocument(); expect(ui.editPermissions.get()).toBeInTheDocument(); }); @@ -421,6 +423,42 @@ it('should show github warning on changing default visibility to admin', async ( expect(ui.defaultVisibilityWarning.get()).toHaveTextContent('.github'); }); +it('should not allow apply permissions for managed projects', async () => { + const user = userEvent.setup(); + renderProjectManagementApp(); + await waitFor(() => expect(ui.row.getAll()).toHaveLength(5)); + const rows = ui.row.getAll(); + expect(rows[1]).toHaveTextContent('local'); + expect(rows[2]).toHaveTextContent('local'); + expect(rows[3]).toHaveTextContent('local'); + expect(rows[4]).not.toHaveTextContent('local'); + expect(ui.checkbox.get(rows[4])).toHaveAttribute('aria-disabled', 'true'); + expect(ui.checkbox.get(rows[1])).not.toHaveAttribute('aria-disabled'); + await user.click(ui.checkAll.get()); + expect(ui.checkbox.get(rows[4])).not.toBeChecked(); + expect(ui.checkbox.get(rows[1])).toBeChecked(); + await act(() => user.click(ui.projectActions.get(rows[4]))); + expect(ui.applyPermissionTemplate.query()).not.toBeInTheDocument(); + expect(ui.restoreAccess.query()).not.toBeInTheDocument(); + expect(ui.editPermissions.query()).not.toBeInTheDocument(); + expect(ui.showPermissions.get()).toBeInTheDocument(); + await act(() => user.click(ui.projectActions.get(rows[1]))); + expect(ui.applyPermissionTemplate.get()).toBeInTheDocument(); + expect(ui.editPermissions.get()).toBeInTheDocument(); + expect(ui.showPermissions.query()).not.toBeInTheDocument(); +}); + +it('should not show local badge for applications and portfolios', async () => { + renderProjectManagementApp(); + await waitFor(() => expect(screen.getAllByText('local')).toHaveLength(3)); + + await selectEvent.select(ui.qualifierFilter.get(), 'qualifiers.VW'); + expect(screen.queryByText('local')).not.toBeInTheDocument(); + + await selectEvent.select(ui.qualifierFilter.get(), 'qualifiers.APP'); + expect(screen.queryByText('local')).not.toBeInTheDocument(); +}); + function renderProjectManagementApp( overrides: Partial = {}, user: Partial = {}, diff --git a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/permission/ws/AddUserActionIT.java b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/permission/ws/AddUserActionIT.java index e61488348d5..6e8cb3c838f 100644 --- a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/permission/ws/AddUserActionIT.java +++ b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/permission/ws/AddUserActionIT.java @@ -213,11 +213,29 @@ public class AddUserActionIT extends BasePermissionWsIT { } @Test - public void fail_when_project_is_managed_and_no_permissions_update() { + public void succeed_when_project_is_managed_and_user_is_sysadmin() { + loginAsAdmin(); ProjectDto project = db.components().insertPrivateProject().getProjectDto(); doThrow(new IllegalStateException("Managed project")).when(managedInstanceChecker).throwIfProjectIsManaged(any(), eq(project.getUuid())); + newRequest() + .setParam(PARAM_USER_LOGIN, user.getLogin()) + .setParam(PARAM_PROJECT_ID, project.getUuid()) + .setParam(PARAM_PERMISSION, UserRole.SCAN) + .execute(); + + assertThat(db.users().selectPermissionsOfUser(user)).isEmpty(); + assertThat(db.users().selectEntityPermissionOfUser(user, project.getUuid())).containsOnly(UserRole.SCAN); + } + + @Test + public void fail_when_project_is_managed_and_user_not_sysadmin() { + ProjectDto project = db.components().insertPrivateProject().getProjectDto(); + userSession.logIn().addProjectPermission(UserRole.ADMIN, project); + + doThrow(new IllegalStateException("Managed project")).when(managedInstanceChecker).throwIfProjectIsManaged(any(), eq(project.getUuid())); + TestRequest request = newRequest() .setParam(PARAM_USER_LOGIN, user.getLogin()) .setParam(PARAM_PROJECT_KEY, project.getKey()) diff --git a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/permission/ws/BasePermissionWsIT.java b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/permission/ws/BasePermissionWsIT.java index 91e1f855f3d..cf2034af5f3 100644 --- a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/permission/ws/BasePermissionWsIT.java +++ b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/permission/ws/BasePermissionWsIT.java @@ -91,7 +91,7 @@ public abstract class BasePermissionWsIT { } protected void loginAsAdmin() { - userSession.logIn().addPermission(ADMINISTER); + userSession.logIn().addPermission(ADMINISTER).setSystemAdministrator(); } protected PermissionTemplateDto selectPermissionTemplate(String name) { diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/permission/ws/AddUserAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/permission/ws/AddUserAction.java index df0a68a3866..a99a554611d 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/permission/ws/AddUserAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/permission/ws/AddUserAction.java @@ -90,10 +90,10 @@ public class AddUserAction implements PermissionsWsAction { try (DbSession dbSession = dbClient.openSession(false)) { String userLogin = request.mandatoryParam(PARAM_USER_LOGIN); EntityDto entityDto = wsSupport.findEntity(dbSession, request); - if (entityDto != null && entityDto.isProject()) { + checkProjectAdmin(userSession, configuration, entityDto); + if (!userSession.isSystemAdministrator() && entityDto != null && entityDto.isProject()) { managedInstanceChecker.throwIfProjectIsManaged(dbSession, entityDto.getUuid()); } - checkProjectAdmin(userSession, configuration, entityDto); UserId user = wsSupport.findUser(dbSession, userLogin); UserPermissionChange change = new UserPermissionChange( diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/permission/ws/RemoveUserAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/permission/ws/RemoveUserAction.java index 76a26d23e41..a2e83772ca9 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/permission/ws/RemoveUserAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/permission/ws/RemoveUserAction.java @@ -89,11 +89,11 @@ public class RemoveUserAction implements PermissionsWsAction { wsSupport.checkRemovingOwnAdminRight(userSession, userIdDto, permission); EntityDto entityDto = wsSupport.findEntity(dbSession, request); - if (entityDto != null) { - managedInstanceChecker.throwIfUserAndProjectAreManaged(dbSession, userIdDto.getUuid(), entityDto.getUuid()); - } wsSupport.checkRemovingOwnBrowsePermissionOnPrivateProject(userSession, entityDto, permission, userIdDto); wsSupport.checkPermissionManagementAccess(userSession, entityDto); + if (entityDto != null && entityDto.isProject()) { + managedInstanceChecker.throwIfUserAndProjectAreManaged(dbSession, userIdDto.getUuid(), entityDto.getUuid()); + } UserPermissionChange change = new UserPermissionChange( PermissionChange.Operation.REMOVE, permission, 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 3392c1cb438..4801252f199 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -272,6 +272,7 @@ default_error_message=The request cannot be processed. Try again later. default_save_field_error_message=This field cannot be saved. Try again later. default_severity=Default severity edit_permissions=Edit Permissions +show_permissions=Show Permissions facet_might_have_more_results=There might be more results, try another set of filters to see them. false_positive=False positive go_back_to_homepage=Go back to the homepage @@ -594,7 +595,7 @@ roles.page.description_portfolio=Grant and revoke portfolio-level permissions. P roles.page.description_application=Grant and revoke application-level permissions. Permissions can be granted to groups or individual users. roles.page.description.github=GitHub-provisioned project permissions are read-only for users provisioned from GitHub. For non-GitHub users, the permissions can only be removed. project_permission.github_managed=Provisioned from GitHub -project_permission.local_project_with_github_provisioning=GitHub-provisioned project permissions are read-only for GitHub users. For local users, the permissions can only be removed. +project_permission.local_project_with_github_provisioning=Please note that this project is not linked to GitHub. Bind it to GitHub to take advantage of the provisioning of permissions. 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