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',
export interface Project extends ProjectBase {
lastAnalysisDate?: string;
+ managed?: boolean;
}
export interface SearchProjectsParameters extends BaseSearchProjectsParameters {
};
onAllSelected = () => {
- this.setState(({ projects }) => ({ selection: projects.map((project) => project.key) }));
+ this.setState(({ projects }) => ({
+ selection: projects.filter((p) => !p.managed).map((project) => project.key),
+ }));
};
onAllDeselected = () => {
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';
selected: boolean;
}
-export default class ProjectRow extends React.PureComponent<Props> {
- 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 (
- <tr data-project-key={project.key}>
- <td className="thin">
- <Checkbox
- label={translateWithParameters('projects_management.select_project', project.name)}
- checked={selected}
- onCheck={this.handleProjectCheck}
- />
- </td>
+ return (
+ <tr data-project-key={project.key}>
+ <td className="thin">
+ <Checkbox
+ label={translateWithParameters('projects_management.select_project', project.name)}
+ checked={selected}
+ disabled={project.managed}
+ onCheck={handleProjectCheck}
+ />
+ </td>
- <td className="nowrap hide-overflow project-row-text-cell">
- <Link
- className="link-no-underline"
- to={getComponentOverviewUrl(project.key, project.qualifier)}
- >
- <QualifierIcon className="little-spacer-right" qualifier={project.qualifier} />
+ <td className="nowrap hide-overflow project-row-text-cell">
+ <Link
+ className="link-no-underline"
+ to={getComponentOverviewUrl(project.key, project.qualifier)}
+ >
+ <QualifierIcon className="little-spacer-right" qualifier={project.qualifier} />
- <Tooltip overlay={project.name} placement="left">
- <span>{project.name}</span>
- </Tooltip>
- </Link>
- </td>
+ <Tooltip overlay={project.name} placement="left">
+ <span>{project.name}</span>
+ </Tooltip>
+ </Link>
+ {project.qualifier === ComponentQualifier.Project && !project.managed && (
+ <span className="badge sw-ml-1">{translate('local')}</span>
+ )}
+ </td>
- <td className="thin nowrap">
- <PrivacyBadgeContainer qualifier={project.qualifier} visibility={project.visibility} />
- </td>
+ <td className="thin nowrap">
+ <PrivacyBadgeContainer qualifier={project.qualifier} visibility={project.visibility} />
+ </td>
- <td className="nowrap hide-overflow project-row-text-cell">
- <Tooltip overlay={project.key} placement="left">
- <span className="note">{project.key}</span>
- </Tooltip>
- </td>
+ <td className="nowrap hide-overflow project-row-text-cell">
+ <Tooltip overlay={project.key} placement="left">
+ <span className="note">{project.key}</span>
+ </Tooltip>
+ </td>
- <td className="thin nowrap text-right">
- {project.lastAnalysisDate ? (
- <DateFormatter date={project.lastAnalysisDate} />
- ) : (
- <span className="note">—</span>
- )}
- </td>
+ <td className="thin nowrap text-right">
+ {project.lastAnalysisDate ? (
+ <DateFormatter date={project.lastAnalysisDate} />
+ ) : (
+ <span className="note">—</span>
+ )}
+ </td>
- <td className="thin nowrap">
- <ProjectRowActions currentUser={this.props.currentUser} project={project} />
- </td>
- </tr>
- );
- }
+ <td className="thin nowrap">
+ <ProjectRowActions currentUser={currentUser} project={project} />
+ </td>
+ </tr>
+ );
}
* 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';
project: Project;
}
-interface State {
- applyTemplateModal: boolean;
- hasAccess?: boolean;
- loading: boolean;
- restoreAccessModal: boolean;
-}
-
-export default class ProjectRowActions extends React.PureComponent<Props, State> {
- 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<boolean | undefined>(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 (
- <>
- <ActionsDropdown
- label={translateWithParameters(
- 'projects_management.show_actions_for_x',
- this.props.project.name
- )}
- onOpen={this.handleDropdownOpen}
- >
- {loading ? (
- <ActionsDropdownItem>
- <DeferredSpinner />
- </ActionsDropdownItem>
- ) : (
- <>
- {hasAccess === true && (
- <ActionsDropdownItem
- className="js-edit-permissions"
- to={getComponentPermissionsUrl(this.props.project.key)}
- >
- {translate('edit_permissions')}
- </ActionsDropdownItem>
- )}
-
- {hasAccess === false && (
- <ActionsDropdownItem
- className="js-restore-access"
- onClick={this.handleRestoreAccessClick}
- >
- {translate('global_permissions.restore_access')}
- </ActionsDropdownItem>
- )}
- </>
- )}
+ return (
+ <>
+ <ActionsDropdown
+ label={translateWithParameters('projects_management.show_actions_for_x', project.name)}
+ onOpen={handleDropdownOpen}
+ >
+ {loading ? (
+ <ActionsDropdownItem>
+ <DeferredSpinner />
+ </ActionsDropdownItem>
+ ) : (
+ <>
+ {hasAccess === true && (
+ <ActionsDropdownItem
+ className="js-edit-permissions"
+ to={getComponentPermissionsUrl(project.key)}
+ >
+ {translate(project.managed ? 'show_permissions' : 'edit_permissions')}
+ </ActionsDropdownItem>
+ )}
+
+ {hasAccess === false && !project.managed && (
+ <ActionsDropdownItem
+ className="js-restore-access"
+ onClick={() => setRestoreAccessModal(true)}
+ >
+ {translate('global_permissions.restore_access')}
+ </ActionsDropdownItem>
+ )}
+ </>
+ )}
+ {!project.managed && (
<ActionsDropdownItem
className="js-apply-template"
- onClick={this.handleApplyTemplateClick}
+ onClick={() => setApplyTemplateModal(true)}
>
{translate('projects_role.apply_template')}
</ActionsDropdownItem>
- </ActionsDropdown>
-
- {this.state.restoreAccessModal && (
- <RestoreAccessModal
- currentUser={this.props.currentUser}
- onClose={this.handleRestoreAccessClose}
- onRestoreAccess={this.handleRestoreAccessDone}
- project={this.props.project}
- />
)}
-
- {this.state.applyTemplateModal && (
- <ApplyTemplate onClose={this.handleApplyTemplateClose} project={this.props.project} />
- )}
- </>
- );
- }
+ </ActionsDropdown>
+
+ {restoreAccessModal && (
+ <RestoreAccessModal
+ currentUser={currentUser}
+ onClose={() => setRestoreAccessModal(false)}
+ onRestoreAccess={handleRestoreAccessDone}
+ project={project}
+ />
+ )}
+
+ {applyTemplateModal && (
+ <ApplyTemplate onClose={() => setApplyTemplateModal(false)} project={project} />
+ )}
+ </>
+ );
}
* 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';
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'),
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());
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();
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();
});
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<AppState> = {},
user: Partial<LoggedInUser> = {},
}
@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())
}
protected void loginAsAdmin() {
- userSession.logIn().addPermission(ADMINISTER);
+ userSession.logIn().addPermission(ADMINISTER).setSystemAdministrator();
}
protected PermissionTemplateDto selectPermissionTemplate(String name) {
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(
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,
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
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