diff --git a/frontend/src/component/project/ProjectAccess/ProjectAccessTable/ProjectAccessTable.tsx b/frontend/src/component/project/ProjectAccess/ProjectAccessTable/ProjectAccessTable.tsx
index fc5d16d0bf..d953a0bad2 100644
--- a/frontend/src/component/project/ProjectAccess/ProjectAccessTable/ProjectAccessTable.tsx
+++ b/frontend/src/component/project/ProjectAccess/ProjectAccessTable/ProjectAccessTable.tsx
@@ -232,12 +232,8 @@ export const ProjectAccessTable: VFC = () => {
? 'group'
: 'user'
}/${row.entity.id}`}
- disabled={access?.rows.length === 1}
tooltipProps={{
- title:
- access?.rows.length === 1
- ? 'Cannot edit access. A project must have at least one owner'
- : 'Edit access',
+ title: 'Edit access',
}}
>
@@ -253,12 +249,8 @@ export const ProjectAccessTable: VFC = () => {
setSelectedRow(row);
setRemoveOpen(true);
}}
- disabled={access?.rows.length === 1}
tooltipProps={{
- title:
- access?.rows.length === 1
- ? 'Cannot remove access. A project must have at least one owner'
- : 'Remove access',
+ title: 'Remove access',
}}
>
diff --git a/src/lib/error/index.ts b/src/lib/error/index.ts
index 6400ed93f2..ab80ac3d55 100644
--- a/src/lib/error/index.ts
+++ b/src/lib/error/index.ts
@@ -10,7 +10,6 @@ import PermissionError from './permission-error';
import { OperationDeniedError } from './operation-denied-error';
import UserTokenError from './used-token-error';
import RoleInUseError from './role-in-use-error';
-import ProjectWithoutOwnerError from './project-without-owner-error';
import PasswordUndefinedError from './password-undefined';
import PasswordMismatchError from './password-mismatch';
import PatternError from './pattern-error';
@@ -32,7 +31,6 @@ export {
OperationDeniedError,
UserTokenError,
RoleInUseError,
- ProjectWithoutOwnerError,
PasswordUndefinedError,
PatternError,
PasswordMismatchError,
diff --git a/src/lib/error/project-without-owner-error.ts b/src/lib/error/project-without-owner-error.ts
deleted file mode 100644
index ad2bf6d19c..0000000000
--- a/src/lib/error/project-without-owner-error.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import { type ApiErrorSchema, UnleashError } from './unleash-error';
-
-export default class ProjectWithoutOwnerError extends UnleashError {
- statusCode = 409;
-
- constructor() {
- super('A project must have at least one owner');
- }
-
- toJSON(): ApiErrorSchema {
- return {
- ...super.toJSON(),
- details: [
- {
- message: this.message,
- validationErrors: [],
- },
- ],
- };
- }
-}
diff --git a/src/lib/error/unleash-error.test.ts b/src/lib/error/unleash-error.test.ts
index 4b6d328b4b..f0323621d3 100644
--- a/src/lib/error/unleash-error.test.ts
+++ b/src/lib/error/unleash-error.test.ts
@@ -10,7 +10,6 @@ import PermissionError from './permission-error';
import OwaspValidationError from './owasp-validation-error';
import IncompatibleProjectError from './incompatible-project-error';
import PasswordUndefinedError from './password-undefined';
-import ProjectWithoutOwnerError from './project-without-owner-error';
import NotFoundError from './notfound-error';
import { validateString } from '../util/validators/constraint-types';
import { fromLegacyError } from './from-legacy-error';
@@ -671,20 +670,6 @@ describe('Error serialization special cases', () => {
],
});
});
-
- it('ProjectWithoutOwnerError: adds `validationErrors: []` to the `details` list', () => {
- const error = new ProjectWithoutOwnerError();
- const json = error.toJSON();
-
- expect(json).toMatchObject({
- details: [
- {
- validationErrors: [],
- message: json.message,
- },
- ],
- });
- });
});
describe('Stack traces', () => {
diff --git a/src/lib/features/project/project-service.e2e.test.ts b/src/lib/features/project/project-service.e2e.test.ts
index 47ea9bc1d3..15656830dc 100644
--- a/src/lib/features/project/project-service.e2e.test.ts
+++ b/src/lib/features/project/project-service.e2e.test.ts
@@ -1579,225 +1579,6 @@ test('should able to assign role without existing members', async () => {
expect(testUsers).toHaveLength(1);
});
-describe('ensure project has at least one owner', () => {
- test('should not remove user from the project', async () => {
- const project = {
- id: 'remove-users-not-allowed',
- name: 'New project',
- description: 'Blah',
- mode: 'open' as const,
- defaultStickiness: 'clientId',
- };
- await projectService.createProject(project, user, auditUser);
-
- const roles = await stores.roleStore.getRolesForProject(project.id);
- const ownerRole = roles.find((r) => r.name === RoleName.OWNER)!;
-
- await expect(async () => {
- await projectService.removeUser(
- project.id,
- ownerRole.id,
- user.id,
- auditUser,
- );
- }).rejects.toThrowError(
- new Error('A project must have at least one owner'),
- );
-
- await expect(async () => {
- await projectService.removeUserAccess(
- project.id,
- user.id,
- auditUser,
- );
- }).rejects.toThrowError(
- new Error('A project must have at least one owner'),
- );
- });
-
- test('should be able to remove member user from the project when another is owner', async () => {
- const project = {
- id: 'remove-users-members-allowed',
- name: 'New project',
- description: 'Blah',
- mode: 'open' as const,
- defaultStickiness: 'clientId',
- };
- await projectService.createProject(project, user, auditUser);
-
- const memberRole = await stores.roleStore.getRoleByName(
- RoleName.MEMBER,
- );
-
- const memberUser = await stores.userStore.insert({
- name: 'Some Name',
- email: 'member@getunleash.io',
- });
-
- await projectService.addAccess(
- project.id,
- [memberRole.id],
- [],
- [memberUser.id],
- auditUser,
- );
-
- const usersBefore = await projectService.getProjectUsers(project.id);
- await projectService.removeUserAccess(
- project.id,
- memberUser.id,
- auditUser,
- );
- const usersAfter = await projectService.getProjectUsers(project.id);
- expect(usersBefore).toHaveLength(2);
- expect(usersAfter).toHaveLength(1);
- });
-
- test('should not update role for user on project when she is the owner', async () => {
- const project = {
- id: 'update-users-not-allowed',
- name: 'New project',
- description: 'Blah',
- mode: 'open' as const,
- defaultStickiness: 'clientId',
- };
- await projectService.createProject(project, user, auditUser);
-
- const projectMember1 = await stores.userStore.insert({
- name: 'Some Member',
- email: 'update991@getunleash.io',
- });
-
- const memberRole = await stores.roleStore.getRoleByName(
- RoleName.MEMBER,
- );
-
- await projectService.addAccess(
- project.id,
- [memberRole.id],
- [], // no groups
- [projectMember1.id],
- auditUser,
- );
-
- await expect(async () => {
- await projectService.changeRole(
- project.id,
- memberRole.id,
- user.id,
- auditUser,
- );
- }).rejects.toThrowError(
- new Error('A project must have at least one owner'),
- );
-
- await expect(async () => {
- await projectService.setRolesForUser(
- project.id,
- user.id,
- [memberRole.id],
- auditUser,
- );
- }).rejects.toThrowError(
- new Error('A project must have at least one owner'),
- );
- });
-
- async function projectWithGroupOwner(projectId: string) {
- const project = {
- id: projectId,
- name: 'New project',
- description: 'Blah',
- mode: 'open' as const,
- defaultStickiness: 'clientId',
- };
- await projectService.createProject(project, user, auditUser);
-
- const roles = await stores.roleStore.getRolesForProject(project.id);
- const ownerRole = roles.find((r) => r.name === RoleName.OWNER)!;
-
- await projectService.addGroup(
- project.id,
- ownerRole.id,
- group.id,
- auditUser,
- );
-
- // this should be fine, leaving the group as the only owner
- // note group has zero members, but it still acts as an owner
- await projectService.removeUser(
- project.id,
- ownerRole.id,
- user.id,
- auditUser,
- );
-
- return {
- project,
- group,
- ownerRole,
- };
- }
-
- test('should not remove group from the project', async () => {
- const { project, group, ownerRole } = await projectWithGroupOwner(
- 'remove-group-not-allowed',
- );
-
- await expect(async () => {
- await projectService.removeGroup(
- project.id,
- ownerRole.id,
- group.id,
- auditUser,
- );
- }).rejects.toThrowError(
- new Error('A project must have at least one owner'),
- );
-
- await expect(async () => {
- await projectService.removeGroupAccess(
- project.id,
- group.id,
- auditUser,
- );
- }).rejects.toThrowError(
- new Error('A project must have at least one owner'),
- );
- });
-
- test('should not update role for group on project when she is the owner', async () => {
- const { project, group } = await projectWithGroupOwner(
- 'update-group-not-allowed',
- );
- const memberRole = await stores.roleStore.getRoleByName(
- RoleName.MEMBER,
- );
-
- await expect(async () => {
- await projectService.changeGroupRole(
- project.id,
- memberRole.id,
- group.id,
- auditUser,
- );
- }).rejects.toThrowError(
- new Error('A project must have at least one owner'),
- );
-
- await expect(async () => {
- await projectService.setRolesForGroup(
- project.id,
- group.id,
- [memberRole.id],
- auditUser,
- );
- }).rejects.toThrowError(
- new Error('A project must have at least one owner'),
- );
- });
-});
-
test('Should allow bulk update of group permissions', async () => {
const project = {
id: 'bulk-update-project',
diff --git a/src/lib/features/project/project-service.test.ts b/src/lib/features/project/project-service.test.ts
index 34d9dc22e1..4ca7f09b58 100644
--- a/src/lib/features/project/project-service.test.ts
+++ b/src/lib/features/project/project-service.test.ts
@@ -2,7 +2,6 @@ import { createTestConfig } from '../../../test/config/test-config';
import { BadDataError } from '../../error';
import { type IBaseEvent, RoleName, TEST_AUDIT_USER } from '../../types';
import { createFakeProjectService } from './createProjectService';
-import ProjectService from './project-service';
describe('enterprise extension: enable change requests', () => {
const createService = () => {
@@ -301,44 +300,4 @@ describe('enterprise extension: enable change requests', () => {
),
).resolves.toBeTruthy();
});
-
- test('has at least one owner after deletion when group has the role and a project user for role exists', async () => {
- const config = createTestConfig();
- const projectId = 'fake-project-id';
- const service = new ProjectService(
- {
- projectStore: {} as any,
- projectOwnersReadModel: {} as any,
- projectFlagCreatorsReadModel: {} as any,
- eventStore: {} as any,
- featureToggleStore: {} as any,
- environmentStore: {} as any,
- featureEnvironmentStore: {} as any,
- accountStore: {} as any,
- projectStatsStore: {} as any,
- projectReadModel: {} as any,
- onboardingReadModel: {} as any,
- },
- config,
- {
- getProjectUsersForRole: async () =>
- Promise.resolve([{ id: 1 } as any]),
- } as any,
- {} as any,
- {
- getProjectGroups: async () =>
- Promise.resolve([{ roles: [2, 5] } as any]),
- } as any,
- {} as any,
- {} as any,
- {} as any,
- {} as any,
- );
-
- await service.validateAtLeastOneOwner(projectId, {
- id: 5,
- name: 'Owner',
- type: 'Owner',
- });
- });
});
diff --git a/src/lib/features/project/project-service.ts b/src/lib/features/project/project-service.ts
index d21f0096bc..8510c33438 100644
--- a/src/lib/features/project/project-service.ts
+++ b/src/lib/features/project/project-service.ts
@@ -62,7 +62,6 @@ import type {
} from '../../types/stores/access-store';
import type FeatureToggleService from '../feature-toggle/feature-toggle-service';
import IncompatibleProjectError from '../../error/incompatible-project-error';
-import ProjectWithoutOwnerError from '../../error/project-without-owner-error';
import { arraysHaveSameItems } from '../../util';
import type { GroupService } from '../../services/group-service';
import type { IGroupRole } from '../../types/group';
@@ -665,8 +664,6 @@ export default class ProjectService {
): Promise {
const role = await this.findProjectRole(projectId, roleId);
- await this.validateAtLeastOneOwner(projectId, role);
-
await this.accessService.removeUserFromRole(userId, role.id, projectId);
const user = await this.accountStore.get(userId);
@@ -695,14 +692,6 @@ export default class ProjectService {
userId,
);
- const ownerRole = await this.accessService.getRoleByName(
- RoleName.OWNER,
- );
-
- if (existingRoles.includes(ownerRole.id)) {
- await this.validateAtLeastOneOwner(projectId, ownerRole);
- }
-
await this.accessService.removeUserAccess(projectId, userId);
await this.eventService.storeEvent(
@@ -727,14 +716,6 @@ export default class ProjectService {
groupId,
);
- const ownerRole = await this.accessService.getRoleByName(
- RoleName.OWNER,
- );
-
- if (existingRoles.includes(ownerRole.id)) {
- await this.validateAtLeastOneOwner(projectId, ownerRole);
- }
-
await this.accessService.removeGroupAccess(projectId, groupId);
await this.eventService.storeEvent(
@@ -804,8 +785,6 @@ export default class ProjectService {
undefined,
);
- await this.validateAtLeastOneOwner(projectId, role);
-
await this.accessService.removeGroupFromRole(
group.id,
role.id,
@@ -967,15 +946,6 @@ export default class ProjectService {
projectId,
userId,
);
- const ownerRole = await this.accessService.getRoleByName(
- RoleName.OWNER,
- );
-
- const hasOwnerRole = includes(currentRoles, ownerRole);
- const isRemovingOwnerRole = !includes(newRoles, ownerRole);
- if (hasOwnerRole && isRemovingOwnerRole) {
- await this.validateAtLeastOneOwner(projectId, ownerRole);
- }
const isAllowedToAssignRoles = await this.isAllowedToAddAccess(
auditUser,
projectId,
@@ -1019,14 +989,6 @@ export default class ProjectService {
groupId,
);
- const ownerRole = await this.accessService.getRoleByName(
- RoleName.OWNER,
- );
- const hasOwnerRole = includes(currentRoles, ownerRole);
- const isRemovingOwnerRole = !includes(newRoles, ownerRole);
- if (hasOwnerRole && isRemovingOwnerRole) {
- await this.validateAtLeastOneOwner(projectId, ownerRole);
- }
const isAllowedToAssignRoles = await this.isAllowedToAddAccess(
auditUser,
projectId,
@@ -1088,25 +1050,6 @@ export default class ProjectService {
return role;
}
- async validateAtLeastOneOwner(
- projectId: string,
- currentRole: IRoleDescriptor,
- ): Promise {
- if (currentRole.name === RoleName.OWNER) {
- const users = await this.accessService.getProjectUsersForRole(
- currentRole.id,
- projectId,
- );
- const groups = await this.groupService.getProjectGroups(projectId);
- const roleGroups = groups.filter((g) =>
- g.roles?.includes(currentRole.id),
- );
- if (users.length + roleGroups.length < 2) {
- throw new ProjectWithoutOwnerError();
- }
- }
- }
-
/** @deprecated use projectInsightsService instead */
async getDoraMetrics(projectId: string): Promise {
const activeFeatureFlags = (
@@ -1180,7 +1123,6 @@ export default class ProjectService {
// Nothing to do....
return;
}
- await this.validateAtLeastOneOwner(projectId, currentRole);
await this.accessService.updateUserProjectRole(
userId,
@@ -1233,7 +1175,6 @@ export default class ProjectService {
// Nothing to do....
return;
}
- await this.validateAtLeastOneOwner(projectId, currentRole);
await this.accessService.updateGroupProjectRole(
userId,