Skip to content

Commit a111a51

Browse files
committed
chore: remove project at least one owner constraint
1 parent 242b0de commit a111a51

File tree

7 files changed

+2
-367
lines changed

7 files changed

+2
-367
lines changed

frontend/src/component/project/ProjectAccess/ProjectAccessTable/ProjectAccessTable.tsx

+2-10
Original file line numberDiff line numberDiff line change
@@ -232,12 +232,8 @@ export const ProjectAccessTable: VFC = () => {
232232
? 'group'
233233
: 'user'
234234
}/${row.entity.id}`}
235-
disabled={access?.rows.length === 1}
236235
tooltipProps={{
237-
title:
238-
access?.rows.length === 1
239-
? 'Cannot edit access. A project must have at least one owner'
240-
: 'Edit access',
236+
title: 'Edit access',
241237
}}
242238
>
243239
<Edit />
@@ -253,12 +249,8 @@ export const ProjectAccessTable: VFC = () => {
253249
setSelectedRow(row);
254250
setRemoveOpen(true);
255251
}}
256-
disabled={access?.rows.length === 1}
257252
tooltipProps={{
258-
title:
259-
access?.rows.length === 1
260-
? 'Cannot remove access. A project must have at least one owner'
261-
: 'Remove access',
253+
title: 'Remove access',
262254
}}
263255
>
264256
<Delete />

src/lib/error/index.ts

-2
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import PermissionError from './permission-error';
1010
import { OperationDeniedError } from './operation-denied-error';
1111
import UserTokenError from './used-token-error';
1212
import RoleInUseError from './role-in-use-error';
13-
import ProjectWithoutOwnerError from './project-without-owner-error';
1413
import PasswordUndefinedError from './password-undefined';
1514
import PasswordMismatchError from './password-mismatch';
1615
import PatternError from './pattern-error';
@@ -32,7 +31,6 @@ export {
3231
OperationDeniedError,
3332
UserTokenError,
3433
RoleInUseError,
35-
ProjectWithoutOwnerError,
3634
PasswordUndefinedError,
3735
PatternError,
3836
PasswordMismatchError,

src/lib/error/project-without-owner-error.ts

-21
This file was deleted.

src/lib/error/unleash-error.test.ts

-15
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import PermissionError from './permission-error';
1010
import OwaspValidationError from './owasp-validation-error';
1111
import IncompatibleProjectError from './incompatible-project-error';
1212
import PasswordUndefinedError from './password-undefined';
13-
import ProjectWithoutOwnerError from './project-without-owner-error';
1413
import NotFoundError from './notfound-error';
1514
import { validateString } from '../util/validators/constraint-types';
1615
import { fromLegacyError } from './from-legacy-error';
@@ -671,20 +670,6 @@ describe('Error serialization special cases', () => {
671670
],
672671
});
673672
});
674-
675-
it('ProjectWithoutOwnerError: adds `validationErrors: []` to the `details` list', () => {
676-
const error = new ProjectWithoutOwnerError();
677-
const json = error.toJSON();
678-
679-
expect(json).toMatchObject({
680-
details: [
681-
{
682-
validationErrors: [],
683-
message: json.message,
684-
},
685-
],
686-
});
687-
});
688673
});
689674

690675
describe('Stack traces', () => {

src/lib/features/project/project-service.e2e.test.ts

-219
Original file line numberDiff line numberDiff line change
@@ -1579,225 +1579,6 @@ test('should able to assign role without existing members', async () => {
15791579
expect(testUsers).toHaveLength(1);
15801580
});
15811581

1582-
describe('ensure project has at least one owner', () => {
1583-
test('should not remove user from the project', async () => {
1584-
const project = {
1585-
id: 'remove-users-not-allowed',
1586-
name: 'New project',
1587-
description: 'Blah',
1588-
mode: 'open' as const,
1589-
defaultStickiness: 'clientId',
1590-
};
1591-
await projectService.createProject(project, user, auditUser);
1592-
1593-
const roles = await stores.roleStore.getRolesForProject(project.id);
1594-
const ownerRole = roles.find((r) => r.name === RoleName.OWNER)!;
1595-
1596-
await expect(async () => {
1597-
await projectService.removeUser(
1598-
project.id,
1599-
ownerRole.id,
1600-
user.id,
1601-
auditUser,
1602-
);
1603-
}).rejects.toThrowError(
1604-
new Error('A project must have at least one owner'),
1605-
);
1606-
1607-
await expect(async () => {
1608-
await projectService.removeUserAccess(
1609-
project.id,
1610-
user.id,
1611-
auditUser,
1612-
);
1613-
}).rejects.toThrowError(
1614-
new Error('A project must have at least one owner'),
1615-
);
1616-
});
1617-
1618-
test('should be able to remove member user from the project when another is owner', async () => {
1619-
const project = {
1620-
id: 'remove-users-members-allowed',
1621-
name: 'New project',
1622-
description: 'Blah',
1623-
mode: 'open' as const,
1624-
defaultStickiness: 'clientId',
1625-
};
1626-
await projectService.createProject(project, user, auditUser);
1627-
1628-
const memberRole = await stores.roleStore.getRoleByName(
1629-
RoleName.MEMBER,
1630-
);
1631-
1632-
const memberUser = await stores.userStore.insert({
1633-
name: 'Some Name',
1634-
1635-
});
1636-
1637-
await projectService.addAccess(
1638-
project.id,
1639-
[memberRole.id],
1640-
[],
1641-
[memberUser.id],
1642-
auditUser,
1643-
);
1644-
1645-
const usersBefore = await projectService.getProjectUsers(project.id);
1646-
await projectService.removeUserAccess(
1647-
project.id,
1648-
memberUser.id,
1649-
auditUser,
1650-
);
1651-
const usersAfter = await projectService.getProjectUsers(project.id);
1652-
expect(usersBefore).toHaveLength(2);
1653-
expect(usersAfter).toHaveLength(1);
1654-
});
1655-
1656-
test('should not update role for user on project when she is the owner', async () => {
1657-
const project = {
1658-
id: 'update-users-not-allowed',
1659-
name: 'New project',
1660-
description: 'Blah',
1661-
mode: 'open' as const,
1662-
defaultStickiness: 'clientId',
1663-
};
1664-
await projectService.createProject(project, user, auditUser);
1665-
1666-
const projectMember1 = await stores.userStore.insert({
1667-
name: 'Some Member',
1668-
1669-
});
1670-
1671-
const memberRole = await stores.roleStore.getRoleByName(
1672-
RoleName.MEMBER,
1673-
);
1674-
1675-
await projectService.addAccess(
1676-
project.id,
1677-
[memberRole.id],
1678-
[], // no groups
1679-
[projectMember1.id],
1680-
auditUser,
1681-
);
1682-
1683-
await expect(async () => {
1684-
await projectService.changeRole(
1685-
project.id,
1686-
memberRole.id,
1687-
user.id,
1688-
auditUser,
1689-
);
1690-
}).rejects.toThrowError(
1691-
new Error('A project must have at least one owner'),
1692-
);
1693-
1694-
await expect(async () => {
1695-
await projectService.setRolesForUser(
1696-
project.id,
1697-
user.id,
1698-
[memberRole.id],
1699-
auditUser,
1700-
);
1701-
}).rejects.toThrowError(
1702-
new Error('A project must have at least one owner'),
1703-
);
1704-
});
1705-
1706-
async function projectWithGroupOwner(projectId: string) {
1707-
const project = {
1708-
id: projectId,
1709-
name: 'New project',
1710-
description: 'Blah',
1711-
mode: 'open' as const,
1712-
defaultStickiness: 'clientId',
1713-
};
1714-
await projectService.createProject(project, user, auditUser);
1715-
1716-
const roles = await stores.roleStore.getRolesForProject(project.id);
1717-
const ownerRole = roles.find((r) => r.name === RoleName.OWNER)!;
1718-
1719-
await projectService.addGroup(
1720-
project.id,
1721-
ownerRole.id,
1722-
group.id,
1723-
auditUser,
1724-
);
1725-
1726-
// this should be fine, leaving the group as the only owner
1727-
// note group has zero members, but it still acts as an owner
1728-
await projectService.removeUser(
1729-
project.id,
1730-
ownerRole.id,
1731-
user.id,
1732-
auditUser,
1733-
);
1734-
1735-
return {
1736-
project,
1737-
group,
1738-
ownerRole,
1739-
};
1740-
}
1741-
1742-
test('should not remove group from the project', async () => {
1743-
const { project, group, ownerRole } = await projectWithGroupOwner(
1744-
'remove-group-not-allowed',
1745-
);
1746-
1747-
await expect(async () => {
1748-
await projectService.removeGroup(
1749-
project.id,
1750-
ownerRole.id,
1751-
group.id,
1752-
auditUser,
1753-
);
1754-
}).rejects.toThrowError(
1755-
new Error('A project must have at least one owner'),
1756-
);
1757-
1758-
await expect(async () => {
1759-
await projectService.removeGroupAccess(
1760-
project.id,
1761-
group.id,
1762-
auditUser,
1763-
);
1764-
}).rejects.toThrowError(
1765-
new Error('A project must have at least one owner'),
1766-
);
1767-
});
1768-
1769-
test('should not update role for group on project when she is the owner', async () => {
1770-
const { project, group } = await projectWithGroupOwner(
1771-
'update-group-not-allowed',
1772-
);
1773-
const memberRole = await stores.roleStore.getRoleByName(
1774-
RoleName.MEMBER,
1775-
);
1776-
1777-
await expect(async () => {
1778-
await projectService.changeGroupRole(
1779-
project.id,
1780-
memberRole.id,
1781-
group.id,
1782-
auditUser,
1783-
);
1784-
}).rejects.toThrowError(
1785-
new Error('A project must have at least one owner'),
1786-
);
1787-
1788-
await expect(async () => {
1789-
await projectService.setRolesForGroup(
1790-
project.id,
1791-
group.id,
1792-
[memberRole.id],
1793-
auditUser,
1794-
);
1795-
}).rejects.toThrowError(
1796-
new Error('A project must have at least one owner'),
1797-
);
1798-
});
1799-
});
1800-
18011582
test('Should allow bulk update of group permissions', async () => {
18021583
const project = {
18031584
id: 'bulk-update-project',

src/lib/features/project/project-service.test.ts

-41
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { createTestConfig } from '../../../test/config/test-config';
22
import { BadDataError } from '../../error';
33
import { type IBaseEvent, RoleName, TEST_AUDIT_USER } from '../../types';
44
import { createFakeProjectService } from './createProjectService';
5-
import ProjectService from './project-service';
65

76
describe('enterprise extension: enable change requests', () => {
87
const createService = () => {
@@ -301,44 +300,4 @@ describe('enterprise extension: enable change requests', () => {
301300
),
302301
).resolves.toBeTruthy();
303302
});
304-
305-
test('has at least one owner after deletion when group has the role and a project user for role exists', async () => {
306-
const config = createTestConfig();
307-
const projectId = 'fake-project-id';
308-
const service = new ProjectService(
309-
{
310-
projectStore: {} as any,
311-
projectOwnersReadModel: {} as any,
312-
projectFlagCreatorsReadModel: {} as any,
313-
eventStore: {} as any,
314-
featureToggleStore: {} as any,
315-
environmentStore: {} as any,
316-
featureEnvironmentStore: {} as any,
317-
accountStore: {} as any,
318-
projectStatsStore: {} as any,
319-
projectReadModel: {} as any,
320-
onboardingReadModel: {} as any,
321-
},
322-
config,
323-
{
324-
getProjectUsersForRole: async () =>
325-
Promise.resolve([{ id: 1 } as any]),
326-
} as any,
327-
{} as any,
328-
{
329-
getProjectGroups: async () =>
330-
Promise.resolve([{ roles: [2, 5] } as any]),
331-
} as any,
332-
{} as any,
333-
{} as any,
334-
{} as any,
335-
{} as any,
336-
);
337-
338-
await service.validateAtLeastOneOwner(projectId, {
339-
id: 5,
340-
name: 'Owner',
341-
type: 'Owner',
342-
});
343-
});
344303
});

0 commit comments

Comments
 (0)